From 58a55988d9bf5586cda4a86394eee6e47bbe0912 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Fri, 24 Jul 2026 17:07:09 -0300 Subject: [PATCH 01/10] feat(keyless): make keyless the default again and let agents configure a keyless instance MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Restores keyless mode as the default for unauthenticated `clerk init` (reversing PR #268), and extends keyless so an agent can bootstrap and configure Clerk end to end without a Clerk account. Init: - Unauthenticated runs on keyless-capable frameworks use keyless again — human bootstrap and all agent runs. Existing-project human runs still log in. - `--login` forces the authenticated flow; `--keyless` now forces keyless even when signed in. `--keyless --login`, `--keyless --app`, and agent `--login` while signed out are usage errors. - `--template ` pre-configures the keyless application at creation. Operating a keyless instance (Backend API, instance secret key only): - `clerk config pull` and `clerk config patch` cover seven resource groups. - `clerk enable/disable orgs`, `clerk whoami`, and `clerk env pull` work with no account. Billing stays account-only and says why. - Keys are also discovered from `.clerk/.tmp/keyless.json`, so an application a Clerk SDK minted for itself is reachable from the CLI. Account credentials are deliberately not part of the keyless decision: only `--app` or a linked profile selects the account path, so these commands work with or without a platform API key or login session. --- .changeset/keyless-mode-default.md | 25 + .../cli-core/src/commands/billing/README.md | 6 + .../cli-core/src/commands/billing/index.ts | 29 +- .../cli-core/src/commands/config/README.md | 87 +++ .../src/commands/config/apply-patch.ts | 41 +- packages/cli-core/src/commands/config/io.ts | 83 +++ .../src/commands/config/keyless.test.ts | 537 ++++++++++++++++++ .../cli-core/src/commands/config/keyless.ts | 182 ++++++ packages/cli-core/src/commands/config/pull.ts | 19 +- packages/cli-core/src/commands/config/push.ts | 100 ++-- .../cli-core/src/commands/config/schema.ts | 11 +- packages/cli-core/src/commands/env/README.md | 2 + packages/cli-core/src/commands/env/pull.ts | 75 ++- packages/cli-core/src/commands/init/README.md | 57 +- .../cli-core/src/commands/init/heuristics.ts | 5 +- .../cli-core/src/commands/init/index.test.ts | 506 +---------------- packages/cli-core/src/commands/init/index.ts | 125 +++- .../src/commands/init/strategy.test.ts | 455 +++++++++++++++ packages/cli-core/src/commands/orgs/README.md | 11 + packages/cli-core/src/commands/orgs/index.ts | 33 +- .../cli-core/src/commands/whoami/README.md | 26 +- .../cli-core/src/commands/whoami/index.ts | 126 +++- packages/cli-core/src/lib/credential-store.ts | 11 + packages/cli-core/src/lib/keyless-target.ts | 196 +++++++ packages/cli-core/src/lib/keyless.ts | 16 +- .../src/test/integration/lib/harness.ts | 2 + .../cli-core/src/test/lib/init-harness.ts | 186 ++++++ packages/cli-core/src/test/lib/stubs.ts | 15 + 28 files changed, 2304 insertions(+), 663 deletions(-) create mode 100644 .changeset/keyless-mode-default.md create mode 100644 packages/cli-core/src/commands/config/io.ts create mode 100644 packages/cli-core/src/commands/config/keyless.test.ts create mode 100644 packages/cli-core/src/commands/config/keyless.ts create mode 100644 packages/cli-core/src/commands/init/strategy.test.ts create mode 100644 packages/cli-core/src/lib/keyless-target.ts create mode 100644 packages/cli-core/src/test/lib/init-harness.ts diff --git a/.changeset/keyless-mode-default.md b/.changeset/keyless-mode-default.md new file mode 100644 index 000000000..eea8488a9 --- /dev/null +++ b/.changeset/keyless-mode-default.md @@ -0,0 +1,25 @@ +--- +"clerk": minor +--- + +Make keyless mode the default for unauthenticated `clerk init` runs on keyless-capable frameworks, and add a `--login` flag to force the authenticated flow. + +- Bootstrapping a new project while signed out now uses auto-generated temporary development keys instead of prompting for a browser login; running `clerk auth login` later claims the app automatically. Re-running init in an existing project while signed out still triggers the login flow. +- Agent-mode runs while signed out now set up keyless keys with a claim breadcrumb instead of printing manual setup guidance. +- `--keyless` now forces keyless mode even when signed in, including in existing projects. Combining it with `--login` or `--app` exits with a usage error. +- `--login` in agent mode while signed out exits with a usage error, since the browser login needs an interactive terminal. + +Add keyless support to `clerk config pull` and `clerk config patch`, so an unclaimed keyless application can be configured without logging in. + +- When the project isn't linked and no `--app` is passed, both commands use the instance secret key the project already holds and talk to the Backend API instead of the Platform API. This needs no account at all — no login and no `CLERK_PLATFORM_API_KEY`. If credentials happen to be present, the command still works and warns that linking would give the full configuration. +- Keyless payloads name Backend API resources directly: `instance`, `communication`, `restrictions`, `organization_settings`, `protect`, `oauth_application_settings`, and `instance_settings` (`test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist`). Any other top-level key exits with a usage error naming the supported ones. +- `clerk config pull` returns the same envelope; `restrictions` and `instance_settings` are omitted because the Backend API has no read route for them. +- In keyless mode `--dry-run` previews the diff locally without sending anything, `--instance` exits with a usage error (the secret key already targets one instance), and `clerk config put` and `clerk config schema` explain that they need a claimed application. + +Let more commands operate on an unclaimed keyless application, so an agent can bootstrap and configure Clerk end to end without an account. + +- `clerk enable orgs` and `clerk disable orgs` now work without logging in, writing through the instance's own organization settings. `clerk enable/disable billing` still requires a claimed application and now says so directly, because Clerk's Backend API exposes no billing settings. +- `clerk whoami` reports the keyless instance — id, environment, publishable key, and where the key was found — instead of failing with "not logged in". +- `clerk env pull` writes a keyless application's locally-held keys into the project's env file instead of failing with "not linked". +- Every keyless-capable command now also finds the keys a Clerk SDK created for itself in `.clerk/.tmp/keyless.json`, so an application minted by running the dev server is reachable from the CLI. +- `clerk init --template ` pre-configures the keyless application at creation — a `b2b-saas` app comes back with organizations already enabled. Keyless-only; combining it with `--login` exits with a usage error. diff --git a/packages/cli-core/src/commands/billing/README.md b/packages/cli-core/src/commands/billing/README.md index bd29a806d..2b8c00d53 100644 --- a/packages/cli-core/src/commands/billing/README.md +++ b/packages/cli-core/src/commands/billing/README.md @@ -4,6 +4,12 @@ Toggle Clerk billing for organizations and/or users on the linked instance. The handlers are wired to top-level `clerk enable billing` and `clerk disable billing` commands. +**Requires a claimed application.** Unlike `clerk enable/disable orgs`, these +commands cannot run against an unclaimed keyless application: billing settings +exist only in the account-level config document, and Clerk's Backend API exposes +no billing resource an instance secret key could reach. In a keyless project both +commands exit with an `auth_required` error pointing at `clerk auth login`. + For arbitrary billing config edits (plans, trials, payment-method requirements) use `clerk config patch --json '{"billing":{...}}'` until a dedicated `clerk billing settings` command lands. diff --git a/packages/cli-core/src/commands/billing/index.ts b/packages/cli-core/src/commands/billing/index.ts index 15e0b1ef8..93bd19d28 100644 --- a/packages/cli-core/src/commands/billing/index.ts +++ b/packages/cli-core/src/commands/billing/index.ts @@ -1,5 +1,4 @@ -import { resolveAppContext } from "../../lib/config.ts"; -import { throwUsageError } from "../../lib/errors.ts"; +import { CliError, ERROR_CODE, throwUsageError } from "../../lib/errors.ts"; import { isAgent, isHuman } from "../../mode.ts"; import { log } from "../../lib/log.ts"; import { confirm } from "../../lib/prompts.ts"; @@ -8,6 +7,7 @@ import { NEXT_STEPS } from "../../lib/next-steps.ts"; import { withGutter } from "../../lib/spinner.ts"; import { resolveSkillsRunner, runSkillsAdd } from "../../lib/skills.ts"; import { applyConfigPatch } from "../config/apply-patch.ts"; +import { resolveInstanceTarget, type InstanceTarget } from "../../lib/keyless-target.ts"; interface BillingOptions { app?: string; @@ -56,9 +56,26 @@ function describeTargets(targets: Target[]): string { return parts.length === 2 ? `${parts[0]} and ${parts[1]}` : parts[0]!; } +/** + * Billing settings live only in the account-level config document — Clerk's + * Backend API exposes no billing resource — so these commands can't run against + * an unclaimed keyless application the way the org toggles can. + */ +async function resolveBillingTarget(options: BillingOptions): Promise { + const target = await resolveInstanceTarget(options); + if (target.kind === "keyless") { + throw new CliError( + "Billing can only be configured on a claimed application — Clerk's Backend API has no billing settings, so an unclaimed keyless application can't reach them.\n" + + "Run `clerk auth login` to claim this application, then re-run the command.", + { code: ERROR_CODE.AUTH_REQUIRED }, + ); + } + return target; +} + export async function billingEnable(options: BillingOptions): Promise { const targets = parseForTargets(options.for); - const ctx = await resolveAppContext(options); + const target = await resolveBillingTarget(options); const billing: Record = {}; const payload: Record = { billing }; @@ -73,7 +90,7 @@ export async function billingEnable(options: BillingOptions): Promise { await withGutter("Enabling billing", async ({ setNextSteps }) => { const applied = await applyConfigPatch({ - ctx, + target, payload, verb: `Enabling billing for ${describeTargets(targets)}`, successMessage: `Billing enabled for ${describeTargets(targets)}`, @@ -122,7 +139,7 @@ async function offerBillingSkillInstall(options: BillingOptions): Promise export async function billingDisable(options: BillingOptions): Promise { const targets = parseForTargets(options.for); - const ctx = await resolveAppContext(options); + const target = await resolveBillingTarget(options); // No cascade: leave organization_settings untouched. const billing: Record = {}; @@ -131,7 +148,7 @@ export async function billingDisable(options: BillingOptions): Promise { await withGutter("Disabling billing", async () => { await applyConfigPatch({ - ctx, + target, payload: { billing }, verb: `Disabling billing for ${describeTargets(targets)}`, successMessage: `Billing disabled for ${describeTargets(targets)}`, diff --git a/packages/cli-core/src/commands/config/README.md b/packages/cli-core/src/commands/config/README.md index ed5c4b9fe..f5f10d164 100644 --- a/packages/cli-core/src/commands/config/README.md +++ b/packages/cli-core/src/commands/config/README.md @@ -2,6 +2,11 @@ Manage Clerk instance configuration. +Two modes exist, picked automatically: + +- **Account mode** (default) — full instance config document via the Platform API. Used whenever the project is linked or `--app` is passed; requires an account (`clerk auth login` or `CLERK_PLATFORM_API_KEY`). +- **Keyless mode** — a reduced set of settings via the Backend API, using only the instance secret key the project already has on disk. No account, no login, and no platform API key required. See [Keyless mode](#keyless-mode). + ## Commands ### `clerk config pull` @@ -31,6 +36,7 @@ clerk config pull --keys auth_email session - a linked Clerk project in the current directory, or - `--app ` to target an application directly - Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt +- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account #### API Endpoints @@ -69,6 +75,7 @@ clerk config schema --keys auth_email session - a linked Clerk project in the current directory, or - `--app ` to target an application directly - Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt +- Account-only: in an unlinked project holding an instance secret key this exits with an error explaining that the schema describes the account-level config document (see [keyless mode](#keyless-mode)) #### API Endpoints @@ -109,6 +116,7 @@ clerk config patch --file partial-config.json --dry-run - a linked Clerk project in the current directory, or - `--app ` to target an application directly - Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt +- **Or neither**: an unlinked project holding an instance secret key falls back to [keyless mode](#keyless-mode), which needs no account #### API Endpoints @@ -149,9 +157,88 @@ clerk config put --file full-config.json --dry-run - a linked Clerk project in the current directory, or - `--app ` to target an application directly - Authenticated via `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or the interactive human-mode prompt +- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [keyless mode](#keyless-mode)) #### API Endpoints | Method | Endpoint | Description | | ------ | ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `PUT` | `/v1/platform/applications/{appID}/instances/{instanceID}/config` | Replaces the full instance configuration. Sends `?dry_run=true` under `--dry-run` to validate and preview without persisting. Authenticated via `Bearer` token from `CLERK_PLATFORM_API_KEY`. | + +--- + +## Keyless mode + +A keyless application created by `clerk init` has no Clerk account behind it until someone claims it, so the Platform API — which authenticates an _account_ — cannot reach it. `clerk config pull` and `clerk config patch` fall back to Clerk's Backend API, authenticated with the instance secret key the project already keeps locally, so an unclaimed app can be configured without logging in. + +### When it engages + +Both must hold, otherwise the account-authenticated path runs unchanged: + +1. No `--app` was passed. +2. No linked project in the current directory. + +**Account credentials are not part of this decision.** Keyless mode works with or without `CLERK_PLATFORM_API_KEY` and with or without a `clerk auth login` session — the instance secret key is sufficient on its own. What rules it out is an explicit destination (`--app` or a linked profile), because that names an application the local secret key may not belong to. + +When credentials _are_ present and the directory simply isn't linked, the command prints a warning that it's using the reduced key-based view and points at `clerk link`, so the narrower output is never a silent surprise. + +The secret key is resolved the way the app itself would resolve one, and this order is shared by every keyless-capable command (`lib/keyless-target.ts`): + +1. `CLERK_SECRET_KEY`, or the framework's secret key variable (e.g. `NUXT_CLERK_SECRET_KEY`), in the environment +2. `.env`, then `.env.local` — the later file wins +3. `.clerk/.tmp/keyless.json` — the keys a Clerk SDK minted for itself when the app ran with no keys configured + +A key that doesn't start with `sk_` is rejected. The SDK file comes last because SDKs only create their own application when nothing else supplies keys. + +### Payload shape + +The Backend API has no single config document — it exposes independent resources — so keyless payloads name them directly instead of translating between the two shapes. Each top-level key maps 1:1 to one endpoint: + +```sh +clerk config patch --json '{ + "instance": { "support_email": "dev@acme.com" }, + "organization_settings": { "enabled": true } +}' +``` + +| Top-level key | Endpoint | Readable | Covers | +| ---------------------------- | ----------------------------------------- | -------- | ----------------------------------------------------------------------------------- | +| `instance` | `/v1/instance` | Yes | Support email, home URL, allowed origins | +| `communication` | `/v1/instance/communication` | Yes | Blocked country codes and communication settings | +| `restrictions` | `/v1/instance/restrictions` | No | Allowlist / blocklist sign-up restrictions | +| `organization_settings` | `/v1/instance/organization_settings` | Yes | Organizations: enabled, membership limits, domains, creation defaults | +| `protect` | `/v1/instance/protect` | Yes | Bot protection | +| `oauth_application_settings` | `/v1/instance/oauth_application_settings` | Yes | Dynamic OAuth client registration | +| `instance_settings` | `/v1/beta_features/instance_settings` | No | `test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist` | + +Any other top-level key exits with a usage error naming the supported ones — the account-mode keys (`session`, `sign_up`, `auth_email`, …) are not silently translated. + +`instance_settings` is backed by a beta route, and is the only way to reach those four auth-config fields without an account — which is why it's included. + +`clerk config pull` returns the same envelope. `restrictions` is omitted because the Backend API has no read route for it; asking for it by name prints a warning rather than failing. + +`GET /v1/instance` returns a subset of what `PATCH /v1/instance` accepts — `support_email`, for example, is writable but not readable. Fields the read omits have no "before" value to compare against, so they always appear as additions in the diff and never trigger "No changes detected". The write itself is unaffected: patching a field to the value it already holds is a no-op server-side. + +### Differences from account mode + +| Behavior | Account mode | Keyless mode | +| --------------- | ---------------------------------------- | ----------------------------------------------------------------- | +| Coverage | Full config document | Three Backend API resources | +| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance | +| `--dry-run` | Server-side validation of the projection | Local diff only; nothing is sent | +| `config put` | Replaces the whole document | Errors — no full document exists to replace | +| `config schema` | Returns the JSON Schema | Errors — the schema describes the account-level document | + +Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage. + +### API Endpoints (keyless mode) + +All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key. + +| Method | Endpoint | Description | +| ------- | ------------------------------------ | ------------------------------------------------------------------------------------------ | +| `GET` | `/v1/instance` | Reads instance settings for `config pull` and for the pre-write diff. | +| `PATCH` | `/v1/instance` | Updates instance settings. Answers `204` with no body, so the group is re-read afterwards. | +| `PATCH` | `/v1/instance/restrictions` | Updates sign-up restrictions (allowlist, blocklist). Write-only — no read route exists. | +| `GET` | `/v1/instance/organization_settings` | Reads organization settings for `config pull` and for the pre-write diff. | +| `PATCH` | `/v1/instance/organization_settings` | Updates organization settings. | diff --git a/packages/cli-core/src/commands/config/apply-patch.ts b/packages/cli-core/src/commands/config/apply-patch.ts index b3368fee7..2ee195819 100644 --- a/packages/cli-core/src/commands/config/apply-patch.ts +++ b/packages/cli-core/src/commands/config/apply-patch.ts @@ -1,13 +1,20 @@ -import { fetchInstanceConfig, patchInstanceConfig } from "../../lib/plapi.ts"; -import { throwUserAbort, withApiContext } from "../../lib/errors.ts"; +import { throwUserAbort } from "../../lib/errors.ts"; import { withSpinner } from "../../lib/spinner.ts"; import { confirm } from "../../lib/prompts.ts"; import { isHuman } from "../../mode.ts"; import { log } from "../../lib/log.ts"; import { hasConfigChanges, printDiff } from "./push.ts"; +import type { InstanceTarget } from "../../lib/keyless-target.ts"; +import { + assertPayloadWritable, + LOCAL_DRY_RUN_MESSAGE, + readInstanceConfig, + supportsServerDryRun, + writeInstanceConfig, +} from "./io.ts"; export interface ApplyPatchOptions { - ctx: { appId: string; instanceId: string; appLabel: string; instanceLabel: string }; + target: InstanceTarget; payload: Record; verb: string; successMessage: string; @@ -21,13 +28,13 @@ export interface ApplyPatchOptions { /** Fetch + diff + confirm + PATCH, matching `clerk config patch` semantics. */ export async function applyConfigPatch(opts: ApplyPatchOptions): Promise { - const { ctx, payload, verb, successMessage, failureContext, yes, dryRun, warning } = opts; + const { target, payload, verb, successMessage, failureContext, yes, dryRun, warning } = opts; + + assertPayloadWritable(target, payload); const current = opts.currentConfig ?? - (await withSpinner("Fetching current config...", () => - withApiContext(fetchInstanceConfig(ctx.appId, ctx.instanceId), "Failed to fetch config"), - )); + (await withSpinner("Fetching current config...", () => readInstanceConfig(target, payload))); if (!hasConfigChanges(current, payload, true)) { log.info(dryRun ? "[dry-run] No changes detected" : "No changes detected"); @@ -35,8 +42,8 @@ export async function applyConfigPatch(opts: ApplyPatchOptions): Promise - withApiContext( - patchInstanceConfig(ctx.appId, ctx.instanceId, payload, { dryRun }), - dryRun ? "Dry-run failed" : failureContext, - ), + writeInstanceConfig(target, payload, { method: "PATCH", dryRun, failureContext }), ); - log.debug(`plapi: ${JSON.stringify(result)}`); + log.debug(`config: ${JSON.stringify(result)}`); log.success(dryRun ? "[dry-run] Validation passed — no changes applied" : successMessage); return true; } diff --git a/packages/cli-core/src/commands/config/io.ts b/packages/cli-core/src/commands/config/io.ts new file mode 100644 index 000000000..c6907f6d7 --- /dev/null +++ b/packages/cli-core/src/commands/config/io.ts @@ -0,0 +1,83 @@ +/** + * Reading and writing instance configuration, whichever way the instance is + * addressed. + * + * This is the only place that knows an account target and a keyless target + * reach different APIs. Callers take an `InstanceTarget`, ask for a read or a + * write, and never branch on `kind` themselves. + */ + +import { withApiContext } from "../../lib/errors.ts"; +import type { InstanceTarget } from "../../lib/keyless-target.ts"; +import { fetchInstanceConfig, patchInstanceConfig, putInstanceConfig } from "../../lib/plapi.ts"; +import { assertKeylessPayload, patchKeylessConfig, readCurrentGroups } from "./keyless.ts"; + +export type ConfigMethod = "PUT" | "PATCH"; + +/** + * Rejects a payload the target can't accept, before any diff is printed or any + * prompt is shown. Only the keyless path constrains the payload — the account + * API validates the document server-side. + */ +export function assertPayloadWritable( + target: InstanceTarget, + payload: Record, +): void { + if (target.kind === "keyless") assertKeylessPayload(payload); +} + +/** + * Current configuration, limited to what the write will touch. + * + * The account API returns one document regardless of `scope`. The keyless API + * has no document, so `scope` selects which resources to read — passing the + * pending payload keeps the read to the groups being diffed. + */ +export function readInstanceConfig( + target: InstanceTarget, + scope: Record, +): Promise> { + if (target.kind === "keyless") return readCurrentGroups(target.keyless, scope); + + return withApiContext( + fetchInstanceConfig(target.ctx.appId, target.ctx.instanceId), + "Failed to fetch current config", + ); +} + +/** + * Server-side dry run is a Platform API feature. The Backend API has no + * equivalent, so a keyless preview can only be produced locally. + */ +export function supportsServerDryRun(target: InstanceTarget): boolean { + return target.kind === "account"; +} + +export const LOCAL_DRY_RUN_MESSAGE = "[dry-run] Nothing sent — no changes applied"; + +export function writeInstanceConfig( + target: InstanceTarget, + payload: Record, + options: { + method: ConfigMethod; + destructive?: boolean; + dryRun?: boolean; + failureContext: string; + }, +): Promise> { + if (target.kind === "keyless") { + // PUT is rejected before reaching here: there is no full document to + // replace when the instance is addressed by its own key. The payload was + // validated by `assertPayloadWritable` before the diff was shown. + return patchKeylessConfig(target.keyless, payload as Record>); + } + + const apiFn = options.method === "PUT" ? putInstanceConfig : patchInstanceConfig; + return withApiContext( + apiFn(target.ctx.appId, target.ctx.instanceId, payload, { + destructive: options.destructive, + dryRun: options.dryRun, + }), + options.dryRun ? "Dry-run failed" : options.failureContext, + ); +} diff --git a/packages/cli-core/src/commands/config/keyless.test.ts b/packages/cli-core/src/commands/config/keyless.test.ts new file mode 100644 index 000000000..da37a9b94 --- /dev/null +++ b/packages/cli-core/src/commands/config/keyless.test.ts @@ -0,0 +1,537 @@ +import { test, expect, describe, beforeEach, afterEach, spyOn, mock } from "bun:test"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { _setConfigDir, setProfile } from "../../lib/config.ts"; +import { useCaptureLog, credentialStoreStubs, gitStubs, stubFetch } from "../../test/lib/stubs.ts"; + +mock.module("../../lib/credential-store.ts", () => credentialStoreStubs); +mock.module("../../lib/git.ts", () => gitStubs); +mock.module("../../lib/spinner.ts", () => ({ + intro: () => {}, + outro: () => {}, + pausedOutro: () => {}, + bar: () => {}, + withGutter: async ( + _title: string, + fn: (controls: { setNextSteps: (steps: readonly string[]) => void }) => Promise, + ) => fn({ setNextSteps: () => {} }), + withSpinner: async (_msg: string, fn: () => Promise) => fn(), +})); + +const SECRET_KEY = "sk_test_keyless"; +const BAPI_URL = "https://test-bapi.clerk.com"; + +const INSTANCE = { object: "instance", id: "ins_1", support_email: "old@example.com" }; +const ORG_SETTINGS = { object: "organization_settings", enabled: false }; +const COMMUNICATION = { object: "instance_communication", blocked_country_codes: [] }; +const PROTECT = { object: "instance_protect", rules_enabled: false }; +const OAUTH_SETTINGS = { object: "oauth_application_settings", dynamic_registration: false }; + +/** Readable groups keyed by BAPI path, mirroring what a pull collects. */ +const READABLE_BODIES: Record = { + "/v1/instance": INSTANCE, + "/v1/instance/communication": COMMUNICATION, + "/v1/instance/organization_settings": ORG_SETTINGS, + "/v1/instance/protect": PROTECT, + "/v1/instance/oauth_application_settings": OAUTH_SETTINGS, +}; + +const FULL_ENVELOPE = { + instance: INSTANCE, + communication: COMMUNICATION, + organization_settings: ORG_SETTINGS, + protect: PROTECT, + oauth_application_settings: OAUTH_SETTINGS, +}; + +describe("keyless config", () => { + const originalEnv = { ...process.env }; + const originalFetch = globalThis.fetch; + const originalCwd = process.cwd(); + let tempDir: string; + let projectDir: string; + let exitSpy: ReturnType; + const captured = useCaptureLog(); + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "clerk-keyless-config-")); + projectDir = await mkdtemp(join(tmpdir(), "clerk-keyless-project-")); + _setConfigDir(tempDir); + process.env.CLERK_BACKEND_API_URL = BAPI_URL; + delete process.env.CLERK_SECRET_KEY; + delete process.env.CLERK_PLATFORM_API_KEY; + + exitSpy = spyOn(process, "exit").mockImplementation(() => { + throw new Error("process.exit"); + }); + + stubFetch(async (input, init) => { + const path = input.toString().replace(BAPI_URL, ""); + const method = (init?.method ?? "GET").toUpperCase(); + + // PATCH /v1/instance answers 204 with no body; every other group echoes. + if (path === "/v1/instance" && method === "PATCH") { + return new Response(null, { status: 204 }); + } + if (path === "/v1/instance/restrictions" && method === "PATCH") { + return new Response(JSON.stringify({ object: "instance_restrictions", allowlist: true }), { + status: 200, + }); + } + if (path === "/v1/beta_features/instance_settings" && method === "PATCH") { + return new Response(JSON.stringify({ object: "instance_settings", test_mode: true }), { + status: 200, + }); + } + const body = READABLE_BODIES[path]; + if (body) return new Response(JSON.stringify(body), { status: 200 }); + + throw new Error(`Unexpected fetch: ${method} ${path}`); + }); + }); + + afterEach(async () => { + process.chdir(originalCwd); + _setConfigDir(undefined); + process.env = { ...originalEnv }; + globalThis.fetch = originalFetch; + exitSpy.mockRestore(); + await rm(tempDir, { recursive: true, force: true }); + await rm(projectDir, { recursive: true, force: true }); + }); + + async function writeEnv(file: string, contents: string): Promise { + await writeFile(join(projectDir, file), contents); + } + + describe("findLocalSecretKey", () => { + test("returns undefined when the project has no secret key", async () => { + const { findLocalSecretKey } = await import("../../lib/keyless-target.ts"); + expect(await findLocalSecretKey(projectDir)).toBeUndefined(); + }); + + test("reads the key from .env", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + const { findLocalSecretKey } = await import("../../lib/keyless-target.ts"); + + expect(await findLocalSecretKey(projectDir)).toEqual({ + secretKey: SECRET_KEY, + source: ".env", + }); + }); + + test(".env.local wins over .env", async () => { + await writeEnv(".env", "CLERK_SECRET_KEY=sk_test_stale\n"); + await writeEnv(".env.local", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + const { findLocalSecretKey } = await import("../../lib/keyless-target.ts"); + + expect(await findLocalSecretKey(projectDir)).toEqual({ + secretKey: SECRET_KEY, + source: ".env.local", + }); + }); + + test("falls back to the keys an SDK created for itself", async () => { + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", ".tmp", "keyless.json"), + JSON.stringify({ + publishableKey: "pk_test_sdk", + secretKey: SECRET_KEY, + claimUrl: "https://dashboard.clerk.com/apps/claim?token=x", + }), + ); + const { findLocalSecretKey } = await import("../../lib/keyless-target.ts"); + + expect(await findLocalSecretKey(projectDir)).toEqual({ + secretKey: SECRET_KEY, + source: ".clerk/.tmp/keyless.json", + }); + }); + + test("prefers env files over the SDK's own keys", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", ".tmp", "keyless.json"), + JSON.stringify({ secretKey: "sk_test_sdk_stale" }), + ); + const { findLocalSecretKey } = await import("../../lib/keyless-target.ts"); + + expect(await findLocalSecretKey(projectDir)).toEqual({ + secretKey: SECRET_KEY, + source: ".env", + }); + }); + + test("ignores a malformed SDK keyless file", async () => { + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile(join(projectDir, ".clerk", ".tmp", "keyless.json"), "{ not json"); + const { findLocalSecretKey } = await import("../../lib/keyless-target.ts"); + + expect(await findLocalSecretKey(projectDir)).toBeUndefined(); + }); + + test("the environment wins over env files", async () => { + await writeEnv(".env", "CLERK_SECRET_KEY=sk_test_from_file\n"); + process.env.CLERK_SECRET_KEY = SECRET_KEY; + const { findLocalSecretKey } = await import("../../lib/keyless-target.ts"); + + expect(await findLocalSecretKey(projectDir)).toEqual({ + secretKey: SECRET_KEY, + source: "CLERK_SECRET_KEY env var", + }); + }); + }); + + describe("resolveKeylessTarget", () => { + test("resolves for an unlinked project holding a secret key", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); + + expect(await resolveKeylessTarget({ cwd: projectDir })).toEqual({ + secretKey: SECRET_KEY, + source: ".env", + }); + }); + + test("defers to the account path when --app is passed", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); + + expect(await resolveKeylessTarget({ app: "app_1", cwd: projectDir })).toBeUndefined(); + }); + + test("resolves even when a platform API key is set, and says why", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform"; + const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); + + expect(await resolveKeylessTarget({ cwd: projectDir })).toEqual({ + secretKey: SECRET_KEY, + source: ".env", + }); + expect(captured.err).toContain("isn't linked to an application"); + }); + + test("stays quiet about the reduced coverage when there is no account", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); + + await resolveKeylessTarget({ cwd: projectDir }); + + expect(captured.err).not.toContain("isn't linked to an application"); + }); + + test("defers to the account path when the project is linked", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + await setProfile(projectDir, { + workspaceId: "org_1", + appId: "app_1", + instances: { development: "ins_dev" }, + }); + const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); + + expect(await resolveKeylessTarget({ cwd: projectDir })).toBeUndefined(); + }); + + test("defers to the account path when no secret key is present", async () => { + const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); + expect(await resolveKeylessTarget({ cwd: projectDir })).toBeUndefined(); + }); + + test("rejects --instance, which the secret key already determines", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); + + await expect(resolveKeylessTarget({ instance: "prod", cwd: projectDir })).rejects.toThrow( + /--instance is not supported for an unclaimed keyless application/, + ); + }); + + test("rejects a key that is not a secret key", async () => { + await writeEnv(".env", "CLERK_SECRET_KEY=pk_test_not_a_secret\n"); + const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); + + await expect(resolveKeylessTarget({ cwd: projectDir })).rejects.toThrow( + /Expected a secret key starting with/, + ); + }); + }); + + describe("assertKeylessPayload", () => { + test("accepts the supported groups", async () => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => + assertKeylessPayload({ instance: { support_email: "a@b.com" }, restrictions: {} }), + ).not.toThrow(); + }); + + test("rejects unknown keys and names the supported ones", async () => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ session: { lifetime: 10 } })).toThrow( + /Unsupported config key .*session.*\n.*instance, communication, restrictions, organization_settings, protect, oauth_application_settings, instance_settings/s, + ); + }); + + test("rejects a group whose value is not an object", async () => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ instance: "nope" })).toThrow(/must be a JSON object/); + }); + }); + + describe("pullKeylessConfig", () => { + test("returns an envelope of the readable groups", async () => { + const { pullKeylessConfig } = await import("./keyless.ts"); + + const config = await pullKeylessConfig({ secretKey: SECRET_KEY, source: ".env" }); + + expect(config).toEqual(FULL_ENVELOPE); + }); + + test("stays quiet about restrictions on a default pull", async () => { + const { pullKeylessConfig } = await import("./keyless.ts"); + + await pullKeylessConfig({ secretKey: SECRET_KEY, source: ".env" }); + + expect(captured.err).not.toContain("no read route"); + }); + + test("warns that restrictions cannot be read and omits it", async () => { + const { pullKeylessConfig } = await import("./keyless.ts"); + + const config = await pullKeylessConfig({ secretKey: SECRET_KEY, source: ".env" }, [ + "restrictions", + ]); + + expect(config).toEqual({}); + expect(captured.err).toContain("no read route for restrictions"); + }); + + test("rejects unknown keys", async () => { + const { pullKeylessConfig } = await import("./keyless.ts"); + + await expect( + pullKeylessConfig({ secretKey: SECRET_KEY, source: ".env" }, ["session"]), + ).rejects.toThrow(/Unsupported config key/); + }); + }); + + describe("patchKeylessConfig", () => { + test("sends each group to its own endpoint", async () => { + const requests: string[] = []; + stubFetch(async (input, init) => { + const url = input.toString(); + const method = (init?.method ?? "GET").toUpperCase(); + requests.push(`${method} ${url.replace(BAPI_URL, "")}`); + if (url.endsWith("/v1/instance") && method === "PATCH") { + return new Response(null, { status: 204 }); + } + if (url.endsWith("/v1/instance")) { + return new Response(JSON.stringify(INSTANCE), { status: 200 }); + } + return new Response(JSON.stringify(ORG_SETTINGS), { status: 200 }); + }); + const { patchKeylessConfig } = await import("./keyless.ts"); + + const result = await patchKeylessConfig( + { secretKey: SECRET_KEY, source: ".env" }, + { + instance: { support_email: "new@example.com" }, + organization_settings: { enabled: true }, + }, + ); + + expect(requests).toEqual([ + "PATCH /v1/instance", + // 204 carries no body, so the group is re-read for the caller. + "GET /v1/instance", + "PATCH /v1/instance/organization_settings", + ]); + expect(result).toEqual({ instance: INSTANCE, organization_settings: ORG_SETTINGS }); + }); + + test("applies groups in table order, not payload order", async () => { + const requests: string[] = []; + stubFetch(async (input, init) => { + const path = input.toString().replace(BAPI_URL, ""); + const method = (init?.method ?? "GET").toUpperCase(); + if (method === "PATCH") requests.push(path); + if (path === "/v1/instance" && method === "PATCH") + return new Response(null, { status: 204 }); + return new Response(JSON.stringify(READABLE_BODIES[path] ?? {}), { status: 200 }); + }); + const { patchKeylessConfig } = await import("./keyless.ts"); + + await patchKeylessConfig( + { secretKey: SECRET_KEY, source: ".env" }, + { protect: { rules_enabled: true }, instance: { support_email: "a@b.com" } }, + ); + + expect(requests).toEqual(["/v1/instance", "/v1/instance/protect"]); + }); + + test("names the already-applied groups when a later group fails", async () => { + stubFetch(async (input, init) => { + const path = input.toString().replace(BAPI_URL, ""); + const method = (init?.method ?? "GET").toUpperCase(); + if (path === "/v1/instance/protect" && method === "PATCH") { + return new Response(JSON.stringify({ errors: [{ message: "nope" }] }), { status: 422 }); + } + if (path === "/v1/instance" && method === "PATCH") { + return new Response(null, { status: 204 }); + } + return new Response(JSON.stringify(READABLE_BODIES[path] ?? {}), { status: 200 }); + }); + const { patchKeylessConfig } = await import("./keyless.ts"); + + // `withApiContext` attaches the explanation as `error.context`, which the + // global handler prints alongside the API message. + await expect( + patchKeylessConfig( + { secretKey: SECRET_KEY, source: ".env" }, + { instance: { support_email: "a@b.com" }, protect: { rules_enabled: true } }, + ), + ).rejects.toMatchObject({ context: "Failed to update protect (already applied: instance)" }); + }); + + test("returns the response body for groups that answer with one", async () => { + const { patchKeylessConfig } = await import("./keyless.ts"); + + const result = await patchKeylessConfig( + { secretKey: SECRET_KEY, source: ".env" }, + { restrictions: { allowlist: true } }, + ); + + expect(result).toEqual({ + restrictions: { object: "instance_restrictions", allowlist: true }, + }); + }); + }); + + describe("config commands in a keyless project", () => { + beforeEach(async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + process.chdir(projectDir); + }); + + test("pull prints the BAPI envelope without an account", async () => { + const { configPull } = await import("./pull.ts"); + + await configPull({}); + + expect(captured.out).toContain(JSON.stringify(FULL_ENVELOPE, null, 2)); + }); + + test("patch applies the payload without an account", async () => { + const { configPatch } = await import("./push.ts"); + + await configPatch({ json: '{"instance":{"support_email":"new@example.com"}}', yes: true }); + + expect(captured.err).toContain("Config pushed successfully"); + }); + + test("patch --dry-run sends nothing", async () => { + const requests: string[] = []; + stubFetch(async (input, init) => { + requests.push(`${(init?.method ?? "GET").toUpperCase()} ${input.toString()}`); + return new Response(JSON.stringify(INSTANCE), { status: 200 }); + }); + const { configPatch } = await import("./push.ts"); + + await configPatch({ json: '{"instance":{"support_email":"new@example.com"}}', dryRun: true }); + + expect(requests.every((request) => request.startsWith("GET"))).toBe(true); + expect(captured.err).toContain("[dry-run] Nothing sent"); + }); + + test("patch rejects unreachable keys before showing a diff or touching the API", async () => { + let requested = false; + stubFetch(async () => { + requested = true; + return new Response(JSON.stringify(INSTANCE), { status: 200 }); + }); + const { configPatch } = await import("./push.ts"); + + await expect(configPatch({ json: '{"session":{"lifetime":1}}', yes: true })).rejects.toThrow( + /Unsupported config key/, + ); + expect(requested).toBe(false); + expect(captured.err).not.toContain("Updating config"); + }); + + test("patch rejects config keys BAPI cannot reach", async () => { + const { configPatch } = await import("./push.ts"); + + await expect( + configPatch({ json: '{"session":{"lifetime":3600}}', yes: true }), + ).rejects.toThrow(/Unsupported config key/); + }); + + test("put explains that a full replace needs a claimed application", async () => { + const { configPut } = await import("./push.ts"); + + await expect(configPut({ json: '{"instance":{}}', yes: true })).rejects.toThrow( + /Replacing the entire configuration is only available for a claimed application/, + ); + }); + + test("enable orgs applies organization settings without an account", async () => { + const requests: string[] = []; + stubFetch(async (input, init) => { + const path = input.toString().replace(BAPI_URL, ""); + const method = (init?.method ?? "GET").toUpperCase(); + requests.push(`${method} ${path}`); + return new Response(JSON.stringify(READABLE_BODIES[path] ?? ORG_SETTINGS), { status: 200 }); + }); + const { orgsEnable } = await import("../orgs/index.ts"); + + await orgsEnable({ yes: true, maxMembers: "7" }); + + expect(requests).toContain("PATCH /v1/instance/organization_settings"); + expect(captured.err).toContain("Organizations enabled"); + }); + + test("whoami reports the keyless instance instead of demanding a login", async () => { + const { whoami } = await import("../whoami/index.ts"); + + await whoami({ json: true }); + + const payload = JSON.parse(captured.out); + expect(payload.email).toBeNull(); + expect(payload.keyless.instanceId).toBe("ins_1"); + expect(payload.keyless.keySource).toBe(".env"); + }); + + test("env pull writes the locally-held keyless keys", async () => { + await writeEnv( + ".env", + `CLERK_SECRET_KEY=${SECRET_KEY}\nCLERK_PUBLISHABLE_KEY=pk_test_local\n`, + ); + const { pull } = await import("../env/pull.ts"); + + await pull({ cwd: projectDir, file: join(projectDir, ".env.written") }); + + const written = await Bun.file(join(projectDir, ".env.written")).text(); + expect(written).toContain(`CLERK_SECRET_KEY=${SECRET_KEY}`); + expect(captured.err).toContain("Keyless application keys"); + }); + + test("enable billing explains that billing needs a claimed application", async () => { + const { billingEnable } = await import("../billing/index.ts"); + + await expect(billingEnable({ for: ["users"], yes: true })).rejects.toThrow( + /Billing can only be configured on a claimed application/, + ); + }); + + test("schema explains that it needs a claimed application", async () => { + const { configSchema } = await import("./schema.ts"); + + await expect(configSchema({})).rejects.toThrow( + /Config schema is only available for a claimed application/, + ); + }); + }); +}); diff --git a/packages/cli-core/src/commands/config/keyless.ts b/packages/cli-core/src/commands/config/keyless.ts new file mode 100644 index 000000000..e13985082 --- /dev/null +++ b/packages/cli-core/src/commands/config/keyless.ts @@ -0,0 +1,182 @@ +/** + * Keyless config access — reading and updating an unclaimed keyless application + * through Clerk's Backend API, using only its instance secret key. + * + * The account-authenticated path (`lib/plapi.ts`) addresses one config document + * per instance. BAPI has no such document: it exposes independent resources. + * Rather than guess a mapping between the two shapes, the keyless payload + * mirrors BAPI directly — one top-level key per resource — so what you write is + * exactly what gets sent. + * + * Finding and addressing the application itself lives in `lib/keyless-target.ts`, + * which the feature-toggle, whoami, and env commands share. + */ + +import { bapiRequest } from "../../lib/bapi.ts"; +import { ERROR_CODE, throwUsageError, withApiContext } from "../../lib/errors.ts"; +import type { KeylessTarget } from "../../lib/keyless-target.ts"; +import { log } from "../../lib/log.ts"; + +/** + * BAPI resources reachable with an instance secret key, keyed by the name they + * take in a keyless config payload. `readable: false` marks a resource BAPI + * exposes for writes only (no GET route), so it never appears in a pull. + * + * Names follow the `object` field each endpoint returns, so what you write here + * matches what the API calls it. + */ +const KEYLESS_GROUPS = { + instance: { path: "/v1/instance", readable: true }, + communication: { path: "/v1/instance/communication", readable: true }, + restrictions: { path: "/v1/instance/restrictions", readable: false }, + organization_settings: { path: "/v1/instance/organization_settings", readable: true }, + protect: { path: "/v1/instance/protect", readable: true }, + oauth_application_settings: { path: "/v1/instance/oauth_application_settings", readable: true }, + // Backed by a beta route (`/v1/beta_features/instance_settings`) that updates + // the auth config: restricted_to_allowlist, from_email_address, + // progressive_sign_up, test_mode. + instance_settings: { path: "/v1/beta_features/instance_settings", readable: false }, +} as const; + +export type KeylessGroup = keyof typeof KEYLESS_GROUPS; + +export const KEYLESS_GROUP_NAMES = Object.keys(KEYLESS_GROUPS) as KeylessGroup[]; + +function isGroupName(name: string): name is KeylessGroup { + return name in KEYLESS_GROUPS; +} + +function isReadable(name: KeylessGroup): boolean { + return KEYLESS_GROUPS[name].readable; +} + +/** + * Validates caller-supplied names once, at the boundary, so everything + * downstream works with a known group instead of re-checking strings. + */ +function asGroupNames(names: string[]): KeylessGroup[] { + const unknown = names.filter((name) => !isGroupName(name)); + if (unknown.length > 0) { + throwUsageError( + `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed keyless application: ${unknown.join(", ")}.\n` + + `Supported keys: ${KEYLESS_GROUP_NAMES.join(", ")}.`, + ); + } + return names.filter(isGroupName); +} + +/** Rejects payload keys that don't name a BAPI resource, before anything is sent. */ +export function assertKeylessPayload( + payload: Record, +): asserts payload is Record> { + const unknown = Object.keys(payload).filter((key) => !(key in KEYLESS_GROUPS)); + if (unknown.length > 0) { + throwUsageError( + `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed keyless application: ${unknown.join(", ")}.\n` + + `Supported top-level keys: ${KEYLESS_GROUP_NAMES.join(", ")}.\n` + + "Run `clerk auth login` to claim the application and use the full config document.", + ); + } + + for (const [key, value] of Object.entries(payload)) { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throwUsageError( + `Config key \`${key}\` must be a JSON object.`, + undefined, + ERROR_CODE.INVALID_JSON, + ); + } + } +} + +/** + * Reads every readable group into a single envelope. Write-only groups are + * skipped — a caller that asked for one by name is told rather than left to + * wonder where it went. + */ +export async function pullKeylessConfig( + target: KeylessTarget, + keys?: string[], +): Promise> { + const requested = keys?.length ? asGroupNames(keys) : KEYLESS_GROUP_NAMES; + + // Only worth saying when the caller named a write-only group. A default pull + // asks for everything, and reporting the same omission every time is noise. + const unreadable = keys?.length ? requested.filter((name) => !isReadable(name)) : []; + if (unreadable.length > 0) { + log.warn( + `Clerk's Backend API has no read route for ${unreadable.join(", ")} — omitted from the output.`, + ); + } + + const config: Record = {}; + for (const name of requested.filter(isReadable)) { + const response = await withApiContext( + bapiRequest({ method: "GET", path: KEYLESS_GROUPS[name].path, secretKey: target.secretKey }), + `Failed to fetch ${name}`, + ); + config[name] = response.body; + } + + return config; +} + +/** + * Applies each group in the payload to its own BAPI resource and returns what + * the API reported back. `PATCH /v1/instance` answers 204 with no body, so that + * group is re-read to give the caller something to see. + */ +export async function patchKeylessConfig( + target: KeylessTarget, + payload: Record>, +): Promise> { + const results: Record = {}; + + // Each group is its own request and the Backend API has no transaction, so a + // failure part-way through leaves earlier groups applied. Name them in the + // error rather than letting the user guess how far it got. + for (const name of KEYLESS_GROUP_NAMES.filter((group) => group in payload)) { + const group = KEYLESS_GROUPS[name]; + const applied = Object.keys(results); + const context = + applied.length > 0 + ? `Failed to update ${name} (already applied: ${applied.join(", ")})` + : `Failed to update ${name}`; + + const response = await withApiContext( + bapiRequest({ + method: "PATCH", + path: group.path, + secretKey: target.secretKey, + body: JSON.stringify(payload[name]), + }), + context, + ); + + if (response.body) { + results[name] = response.body; + continue; + } + + // A 204 carries nothing to show; re-read so the caller still sees the group. + results[name] = group.readable + ? ( + await withApiContext( + bapiRequest({ method: "GET", path: group.path, secretKey: target.secretKey }), + `Failed to fetch ${name}`, + ) + ).body + : null; + } + + return results; +} + +/** Current state of the groups a payload touches, for diffing before a write. */ +export async function readCurrentGroups( + target: KeylessTarget, + payload: Record, +): Promise> { + const readable = Object.keys(payload).filter(isGroupName).filter(isReadable); + return readable.length > 0 ? pullKeylessConfig(target, readable) : {}; +} diff --git a/packages/cli-core/src/commands/config/pull.ts b/packages/cli-core/src/commands/config/pull.ts index 64a68dbd3..3cd713295 100644 --- a/packages/cli-core/src/commands/config/pull.ts +++ b/packages/cli-core/src/commands/config/pull.ts @@ -1,8 +1,9 @@ -import { resolveAppContext } from "../../lib/config.ts"; import { fetchInstanceConfig } from "../../lib/plapi.ts"; import { withApiContext } from "../../lib/errors.ts"; import { withGutter, withSpinner } from "../../lib/spinner.ts"; import { log } from "../../lib/log.ts"; +import { resolveInstanceTarget } from "../../lib/keyless-target.ts"; +import { pullKeylessConfig } from "./keyless.ts"; interface ConfigPullOptions { app?: string; @@ -13,15 +14,15 @@ interface ConfigPullOptions { export async function configPull(options: ConfigPullOptions): Promise { await withGutter("Pulling configuration", async () => { - const ctx = await resolveAppContext(options); + const target = await resolveInstanceTarget(options); - const config = await withSpinner( - `Pulling config from ${ctx.appLabel} (${ctx.instanceLabel})...`, - () => - withApiContext( - fetchInstanceConfig(ctx.appId, ctx.instanceId, options.keys), - "Failed to fetch config", - ), + const config = await withSpinner(`Pulling config from ${target.label}...`, () => + target.kind === "keyless" + ? pullKeylessConfig(target.keyless, options.keys) + : withApiContext( + fetchInstanceConfig(target.ctx.appId, target.ctx.instanceId, options.keys), + "Failed to fetch config", + ), ); const json = JSON.stringify(config, null, 2); diff --git a/packages/cli-core/src/commands/config/push.ts b/packages/cli-core/src/commands/config/push.ts index 36c61be1c..6dd56f94d 100644 --- a/packages/cli-core/src/commands/config/push.ts +++ b/packages/cli-core/src/commands/config/push.ts @@ -1,12 +1,10 @@ -import { resolveAppContext } from "../../lib/config.ts"; -import { fetchInstanceConfig, putInstanceConfig, patchInstanceConfig } from "../../lib/plapi.ts"; import { isHuman } from "../../mode.ts"; import { + CliError, UserAbortError, isPromptExitError, throwUsageError, throwUserAbort, - withApiContext, ERROR_CODE, } from "../../lib/errors.ts"; import { confirm } from "../../lib/prompts.ts"; @@ -14,6 +12,15 @@ import { dim, bold, red, green } from "../../lib/color.ts"; import { withSpinner, intro, outro, pausedOutro } from "../../lib/spinner.ts"; import { isInsideGutter, log } from "../../lib/log.ts"; import { NEXT_STEPS, printNextSteps } from "../../lib/next-steps.ts"; +import { resolveInstanceTarget } from "../../lib/keyless-target.ts"; +import { + assertPayloadWritable, + LOCAL_DRY_RUN_MESSAGE, + readInstanceConfig, + supportsServerDryRun, + writeInstanceConfig, + type ConfigMethod, +} from "./io.ts"; interface ConfigPushOptions { app?: string; @@ -26,15 +33,9 @@ interface ConfigPushOptions { } type Operation = { - method: "PUT" | "PATCH"; + method: ConfigMethod; verb: string; warning?: string; - apiFn: ( - appId: string, - instId: string, - config: Record, - options?: { destructive?: boolean; dryRun?: boolean }, - ) => Promise>; title: string; }; @@ -42,14 +43,12 @@ const PUT_OP: Operation = { method: "PUT", verb: "Replacing", warning: "This will overwrite the entire instance configuration.", - apiFn: putInstanceConfig, title: "Replacing configuration", }; const PATCH_OP: Operation = { method: "PATCH", verb: "Updating", - apiFn: patchInstanceConfig, title: "Patching configuration", }; @@ -62,26 +61,18 @@ export async function configPatch(options: ConfigPushOptions): Promise { } async function configPush(options: ConfigPushOptions, op: Operation): Promise { - const ctx = await resolveAppContext(options); - const rawInput = await readInput(options); + const target = await resolveInstanceTarget(options); - let configPayload: Record; - try { - configPayload = JSON.parse(rawInput); - } catch { - throwUsageError( - "Invalid JSON input. Please provide valid JSON.", - undefined, - ERROR_CODE.INVALID_JSON, + if (target.kind === "keyless" && op.method === "PUT") { + throw new CliError( + "Replacing the entire configuration is only available for a claimed application — an unclaimed keyless application has no full config document to replace.\n" + + "Use `clerk config patch` to update individual settings, or run `clerk auth login` to claim the application first.", + { code: ERROR_CODE.AUTH_REQUIRED }, ); } - if (typeof configPayload !== "object" || configPayload === null || Array.isArray(configPayload)) { - throwUsageError("Config must be a JSON object.", undefined, ERROR_CODE.INVALID_JSON); - } - - // Strip config_version — it's returned by pull but not accepted by the backend - delete configPayload.config_version; + const configPayload = parsePayload(await readInput(options)); + assertPayloadWritable(target, configPayload); const shouldWrap = !isInsideGutter(); if (shouldWrap) intro(op.title); @@ -89,10 +80,7 @@ async function configPush(options: ConfigPushOptions, op: Operation): Promise - withApiContext( - fetchInstanceConfig(ctx.appId, ctx.instanceId), - "Failed to fetch current config", - ), + readInstanceConfig(target, configPayload), ); delete currentConfig.config_version; @@ -105,9 +93,16 @@ async function configPush(options: ConfigPushOptions, op: Operation): Promise - withApiContext( - op.apiFn(ctx.appId, ctx.instanceId, configPayload, { - destructive: options.destructive, - dryRun: options.dryRun, - }), - options.dryRun ? "Dry-run failed" : "Failed to push config", - ), + writeInstanceConfig(target, configPayload, { + method: op.method, + destructive: options.destructive, + dryRun: options.dryRun, + failureContext: "Failed to push config", + }), ); log.data(JSON.stringify(result, null, 2)); log.success( @@ -160,6 +154,28 @@ async function configPush(options: ConfigPushOptions, op: Operation): Promise { + let payload: Record; + try { + payload = JSON.parse(rawInput); + } catch { + throwUsageError( + "Invalid JSON input. Please provide valid JSON.", + undefined, + ERROR_CODE.INVALID_JSON, + ); + } + + if (typeof payload !== "object" || payload === null || Array.isArray(payload)) { + throwUsageError("Config must be a JSON object.", undefined, ERROR_CODE.INVALID_JSON); + } + + // Strip config_version — it's returned by pull but not accepted by the backend + delete payload.config_version; + return payload; +} + export async function readInput(options: { file?: string; json?: string }): Promise { if (options.json) { return options.json; diff --git a/packages/cli-core/src/commands/config/schema.ts b/packages/cli-core/src/commands/config/schema.ts index 4762f4970..130551c43 100644 --- a/packages/cli-core/src/commands/config/schema.ts +++ b/packages/cli-core/src/commands/config/schema.ts @@ -1,8 +1,9 @@ import { resolveAppContext } from "../../lib/config.ts"; import { fetchInstanceConfigSchema } from "../../lib/plapi.ts"; -import { withApiContext } from "../../lib/errors.ts"; +import { CliError, ERROR_CODE, withApiContext } from "../../lib/errors.ts"; import { withGutter } from "../../lib/spinner.ts"; import { log } from "../../lib/log.ts"; +import { resolveKeylessTarget } from "../../lib/keyless-target.ts"; interface ConfigSchemaOptions { app?: string; @@ -13,6 +14,14 @@ interface ConfigSchemaOptions { export async function configSchema(options: ConfigSchemaOptions): Promise { await withGutter("Fetching configuration schema", async () => { + if (await resolveKeylessTarget(options)) { + throw new CliError( + "Config schema is only available for a claimed application — the schema describes the account-level config document, which an unclaimed keyless application has no access to.\n" + + "Run `clerk auth login` to claim this application, then re-run `clerk config schema`.", + { code: ERROR_CODE.AUTH_REQUIRED }, + ); + } + const ctx = await resolveAppContext(options); log.info(`Pulling config schema from ${ctx.appLabel} (${ctx.instanceLabel})...`); diff --git a/packages/cli-core/src/commands/env/README.md b/packages/cli-core/src/commands/env/README.md index 379b5a2a0..77e28f02a 100644 --- a/packages/cli-core/src/commands/env/README.md +++ b/packages/cli-core/src/commands/env/README.md @@ -2,6 +2,8 @@ Pulls Clerk API keys for the linked instance and merges them into the project's `.env` file. +For an unclaimed **keyless** application there is no account to pull from — its keys only exist on this machine. When the directory isn't linked and no `--app` is passed, `env pull` instead copies the keys it finds locally (env var, `.env`/`.env.local`, or the `.clerk/.tmp/keyless.json` an SDK wrote for itself) into the env file the framework reads, and makes no API call. This is what materializes an SDK-created keyless app into `.env.local`. If only the secret key can be found, it's written and a warning names the missing publishable key. Resolution order lives in [`lib/keyless-target.ts`](../../lib/keyless-target.ts). + ## Usage ```sh diff --git a/packages/cli-core/src/commands/env/pull.ts b/packages/cli-core/src/commands/env/pull.ts index d30fd3d00..d1d6423bc 100644 --- a/packages/cli-core/src/commands/env/pull.ts +++ b/packages/cli-core/src/commands/env/pull.ts @@ -10,6 +10,11 @@ import { isNpmFramework, } from "../../lib/framework.ts"; import { CliError, ERROR_CODE, withApiContext } from "../../lib/errors.ts"; +import { + findLocalPublishableKey, + resolveKeylessTarget, + type KeylessTarget, +} from "../../lib/keyless-target.ts"; import { withGutter, withSpinner } from "../../lib/spinner.ts"; import { log } from "../../lib/log.ts"; @@ -52,6 +57,16 @@ async function resolveTargetFile( export async function pull(options: EnvPullOptions): Promise { await withGutter("Pulling environment variables", async () => { const cwd = options.cwd ?? process.cwd(); + + // A keyless application's keys are already on this machine — that's the only + // place they exist. "Pulling" them means copying what an SDK minted into the + // env file the framework reads, not fetching from an account. + const keyless = await resolveKeylessTarget({ ...options, cwd }); + if (keyless) { + await pullKeylessKeys(cwd, keyless, options.file); + return; + } + const [ctx, preferredEnvFile] = await Promise.all([ resolveAppContext({ ...options, cwd }), detectEnvFile(cwd), @@ -79,22 +94,56 @@ export async function pull(options: EnvPullOptions): Promise { const framework = await detectFramework(cwd); const includeSecretKey = isNpmFramework(framework ?? {}); - const file = Bun.file(targetFile); - const existingContent = (await file.exists()) ? await file.text() : ""; - - const lines = parseEnvFile(existingContent); - const vars: Record = { + await mergeKeysIntoEnvFile(targetFile, { [publishableKeyName]: matched.publishable_key, - }; - if (matched.secret_key && includeSecretKey) { - vars[secretKeyName] = matched.secret_key; - } - const merged = mergeEnvVars(lines, vars); - const output = serializeEnvFile(merged); - - await Bun.write(targetFile, output); + ...(matched.secret_key && includeSecretKey && { [secretKeyName]: matched.secret_key }), + }); }); log.info(`Environment variables written to ${displayPath}`); }); } + +/** Merges keys into an env file, preserving everything already in it. */ +async function mergeKeysIntoEnvFile( + targetFile: string, + vars: Record, +): Promise { + const file = Bun.file(targetFile); + const existingContent = (await file.exists()) ? await file.text() : ""; + + await Bun.write(targetFile, serializeEnvFile(mergeEnvVars(parseEnvFile(existingContent), vars))); +} + +/** + * Writes a keyless application's local keys into the project's env file. The + * publishable key can be missing when an SDK holds only part of the pair; the + * secret key is always present because it's what identified the target. + */ +async function pullKeylessKeys( + cwd: string, + keyless: KeylessTarget, + fileFlag?: string, +): Promise { + const [preferredEnvFile, publishableKeyName, secretKeyName, publishableKey] = await Promise.all([ + detectEnvFile(cwd), + detectPublishableKeyName(cwd), + detectSecretKeyName(cwd), + findLocalPublishableKey(cwd), + ]); + + const targetFile = await resolveTargetFile(cwd, fileFlag, preferredEnvFile); + const displayPath = fileFlag ?? basename(targetFile); + + await mergeKeysIntoEnvFile(targetFile, { + [secretKeyName]: keyless.secretKey, + ...(publishableKey && { [publishableKeyName]: publishableKey }), + }); + + log.info(`Keyless application keys from \`${keyless.source}\` written to ${displayPath}`); + if (!publishableKey) { + log.warn( + `No publishable key found locally — set ${publishableKeyName} manually, or run \`clerk auth login\` to claim the application.`, + ); + } +} diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index 8b330ed02..4e39b9586 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -1,6 +1,6 @@ # Init Command -Initializes Clerk in a project by detecting the framework, installing the SDK, and scaffolding framework-specific boilerplate. By default, init logs the user in (interactively) and links the project to a real Clerk application. Pass `--keyless` to opt into auto-generated temporary development keys instead — useful when you want to scaffold a new project without authenticating. +Initializes Clerk in a project by detecting the framework, installing the SDK, and scaffolding framework-specific boilerplate. When the user is unauthenticated and the framework supports keyless, init defaults to keyless mode — auto-generated temporary development keys that a later `clerk auth login` claims automatically — during bootstrap (new projects) in human mode and in all agent-mode runs. Otherwise init logs the user in (interactively) and links a real Clerk application. `--keyless` forces keyless (even when logged in); `--login` forces the authenticated flow. ## Usage @@ -12,6 +12,8 @@ clerk init --starter clerk init --starter --framework next --pm bun clerk init --starter --framework next --pm bun --name my-app clerk init --starter --framework next --keyless +clerk init --login +clerk init --template b2b-saas clerk init -y clerk init --yes clerk init --no-skills @@ -19,16 +21,18 @@ clerk init --no-skills ## Options -| Option | Description | -| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--framework ` | Framework to set up (skips auto-detection). Valid values: `next`, `astro`, `nuxt`, `tanstack-start`, `react-router`, `vue`, `expo`, `react`, `javascript`, `js`, `express`, `fastify`, `ios`, `android` | -| `--pm ` | Package manager to use. Valid values: `bun`, `pnpm`, `yarn`, `npm`. Skips the PM prompt (bootstrap) or overrides lockfile detection (existing project) | -| `--name ` | Project name for `--starter` (skips prompt). Must be lowercase, no spaces, no path separators | -| `--app ` | Application ID to link (skips the interactive app picker during authenticated linking) | -| `--starter` | Bootstrap a new project from a starter template (runs the framework generator, installs deps, and scaffolds Clerk) | -| `--keyless` | Use auto-generated temporary development keys instead of logging in. Only valid when bootstrapping a new project on a keyless-capable framework | -| `-y, --yes` | Skip y/n confirmation prompts. Authentication is still required — unauthenticated users are prompted to log in via the browser unless `--keyless` is also passed | -| `--no-skills` | Skip the optional agent skills install prompt at the end of init | +| Option | Description | +| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--framework ` | Framework to set up (skips auto-detection). Valid values: `next`, `astro`, `nuxt`, `tanstack-start`, `react-router`, `vue`, `expo`, `react`, `javascript`, `js`, `express`, `fastify`, `ios`, `android` | +| `--pm ` | Package manager to use. Valid values: `bun`, `pnpm`, `yarn`, `npm`. Skips the PM prompt (bootstrap) or overrides lockfile detection (existing project) | +| `--name ` | Project name for `--starter` (skips prompt). Must be lowercase, no spaces, no path separators | +| `--app ` | Application ID to link (skips the interactive app picker during authenticated linking) | +| `--starter` | Bootstrap a new project from a starter template (runs the framework generator, installs deps, and scaffolds Clerk) | +| `--keyless` | Force auto-generated temporary development keys, even when logged in. Only valid on a keyless-capable framework; cannot be combined with `--login` or `--app` | +| `--login` | Force the authenticated flow: log in (interactively if needed) and link a real application instead of keyless keys. Errors in agent mode when unauthenticated (agents can't run OAuth) | +| `--template ` | Pre-configure the keyless application at creation: `b2b-saas`, `b2c-saas`, `native`, `waitlist`. Keyless-only — cannot be combined with `--login` | +| `-y, --yes` | Skip y/n confirmation prompts only. It neither forces nor bypasses keyless — the strategy is picked by auth state, mode, and flags | +| `--no-skills` | Skip the optional agent skills install prompt at the end of init | ## Agent Mode @@ -40,20 +44,22 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful - Project name defaults to the framework's default (e.g. `my-clerk-next-app`) unless `--name` is provided - For keyless-capable frameworks with no `--app` and no linked profile: - When **authenticated**, init creates a real Clerk app named after the project (`package.json#name`, `--name`, or directory basename) and links it. - - When **unauthenticated**, init prints manual setup guidance (pointing to `--keyless` or `clerk auth login`) and exits cleanly. Pass `--keyless` to opt into auto-generated dev keys; init writes a breadcrumb so the next `clerk auth login` claims the app automatically. + - When **unauthenticated**, init uses keyless: the app runs on auto-generated dev keys, and init writes a `.clerk/keyless.json` breadcrumb so the next `clerk auth login` claims the app automatically. - For frameworks that require API keys, init will not pick or create an app in agent mode; pass `--app ` or link the project first to pull real keys +- `--login` while unauthenticated exits with a usage error (agents can't complete the interactive browser login) ## Flow 1. Gathers project context (framework, router variant, TypeScript, `src/` directory, package manager) -2. Determines auth mode: - - **`--keyless`**: opt-in to keyless mode. Only valid on a keyless-capable framework (otherwise init exits with a usage error). The app runs on auto-generated dev keys; init writes a `.clerk/keyless.json` breadcrumb so the next `clerk auth login` claims the app automatically +2. Determines the strategy (in precedence order): + - **`--keyless`**: forces keyless mode, even when logged in. Only valid on a keyless-capable framework, and cannot be combined with `--login` or `--app` (usage errors otherwise). The app runs on auto-generated dev keys; init writes a `.clerk/keyless.json` breadcrumb so the next `clerk auth login` claims the app automatically + - **`--login`**: forces the authenticated flow. In agent mode while unauthenticated this exits with a usage error, since agents can't complete the interactive browser login - **Real app target** (`--app` or linked profile): authenticates, links if needed, and pulls real API keys into `.env` - - **Agent + keyless-capable framework + authenticated + no real app target**: creates a real Clerk app named after the project, links it, and pulls real API keys into `.env` - - **Agent + unauthenticated + no real app target + no `--keyless`**: scaffolds locally and prints manual setup guidance (`--keyless` or `clerk auth login`) - **Agent + non-keyless framework + no real app target**: scaffolds locally and prints manual setup instructions instead of selecting or creating an app - - **Human mode + not authenticated + no `--keyless`**: triggers an interactive `clerk auth login` and links a real app. `-y` does not bypass this — it only suppresses y/n confirmation prompts, not authentication - - **Human mode + existing project + not authenticated**: runs the authenticated flow, which triggers an interactive login so real keys can be pulled + - **Agent + keyless-capable framework + authenticated + no real app target**: creates a real Clerk app named after the project, links it, and pulls real API keys into `.env` + - **Agent + keyless-capable framework + unauthenticated + no real app target**: uses keyless mode — the app runs on auto-generated dev keys and the breadcrumb lets the next `clerk auth login` claim it + - **Human mode + bootstrap + keyless-capable framework + not authenticated**: uses keyless mode + - **Human mode + existing project + not authenticated**: runs the authenticated flow, which triggers an interactive login so real keys can be pulled. `-y` does not bypass this — it only suppresses y/n confirmation prompts, not authentication 3. **Authenticated mode only**: authenticates via `clerk auth login` (skipped if already authenticated) and links the project via `clerk link` (skipped if already linked) 4. Displays detected framework and variant 5. Detects existing auth libraries (NextAuth, Auth0, Supabase, Firebase, Passport, Better Auth, Kinde) and shows migration guidance @@ -96,7 +102,7 @@ Native mobile platforms may not have a `package.json`, so they are detected from A bare `Package.swift` or `build.gradle` is intentionally **not** enough — those also match server-side Swift packages and non-Android JVM projects. For native platforms the Clerk SDK cannot be installed by a JS package manager, so init skips the SDK install step and the scaffold plan prints Swift Package Manager / Gradle install steps instead. The publishable key is configured in source code (`Clerk.configure(...)` / `Clerk.initialize(...)`), so init still pulls keys into the env file and instructs the user to copy the key over. -The **Keyless** column indicates whether the framework's Clerk SDK supports keyless mode (auto-generated temporary dev keys). Keyless mode is opt-in via `--keyless` and is only valid when bootstrapping a new project on a Yes-row framework — passing `--keyless` for a No-row framework or for an existing project exits with a usage error. By default, init authenticates the user (interactively when needed) and links a real app. In agent mode, an authenticated run on a keyless-capable framework creates a real app named after the project and links it; an unauthenticated agent run without `--keyless` prints manual setup guidance instead of selecting or creating an app. +The **Keyless** column indicates whether the framework's Clerk SDK supports keyless mode (auto-generated temporary dev keys). Keyless is the default for unauthenticated runs on Yes-row frameworks — during bootstrap (new projects) in human mode, and in all agent-mode runs. In human mode, an unauthenticated re-run in an existing project still triggers the authenticated flow. `--keyless` forces keyless anywhere a Yes-row framework is detected (existing projects included, even when logged in); passing it for a No-row framework exits with a usage error. In agent mode, an authenticated run on a keyless-capable framework creates a real app named after the project and links it. Package manager is detected from lock files: `bun.lockb`/`bun.lock` → bun, `yarn.lock` → yarn, `pnpm-lock.yaml` → pnpm, else npm. @@ -267,6 +273,19 @@ Implementation lives in [`skills.ts`](./skills.ts). Note that the E2E fixture se See [auth/README.md](../auth/README.md), [link/README.md](../link/README.md), and [env/README.md](../env/README.md) for the API endpoints used by each step. +## Application templates + +`--template ` is forwarded to `POST /v1/accountless_applications`, which pre-configures the application server-side before the first key is used. This is the one-shot way for an agent to get a shaped instance without an account — a `b2b-saas` keyless app comes back with organizations already enabled, where a default one does not. + +| Template | Shape | +| ---------- | -------------------------------- | +| `b2b-saas` | Organizations-first B2B setup | +| `b2c-saas` | Consumer setup with user billing | +| `native` | Native/mobile application | +| `waitlist` | Waitlist sign-up mode | + +The template only applies when an application is created, so it is keyless-only: combining it with `--login` exits with a usage error. Settings can still be changed afterwards with `clerk config patch`, which also works without an account (see [config keyless mode](../config/README.md#keyless-mode)). + ## Keyless breadcrumb In keyless mode, after calling `POST /v1/accountless_applications`, `clerk init` writes `.clerk/keyless.json` to the project root. This file records the claim token extracted from `claim_url` so that `clerk auth login` can automatically claim the temporary application the next time the user authenticates. diff --git a/packages/cli-core/src/commands/init/heuristics.ts b/packages/cli-core/src/commands/init/heuristics.ts index fab992ba9..615568b36 100644 --- a/packages/cli-core/src/commands/init/heuristics.ts +++ b/packages/cli-core/src/commands/init/heuristics.ts @@ -3,7 +3,7 @@ import { mkdir } from "node:fs/promises"; import { dim, cyan, green, yellow, bold } from "../../lib/color.js"; import { printNextSteps } from "../../lib/next-steps.js"; import { log } from "../../lib/log.js"; -import { getValidToken, hasStoredCredentials } from "../../lib/credential-store.js"; +import { getValidToken, hasAccountCredentials } from "../../lib/credential-store.js"; import { fetchUserInfo } from "../../lib/token-exchange.js"; import { printFindings } from "./scan.js"; import { pmInstallCommand } from "../../lib/package-manager.js"; @@ -147,8 +147,7 @@ export async function getAuthenticatedEmail(): Promise { * calls, which surface real errors instead of swallowing them. */ export async function isAuthenticated(): Promise { - if (process.env.CLERK_PLATFORM_API_KEY) return true; - return hasStoredCredentials(); + return hasAccountCredentials(); } export function printKeylessInfo(envFile: string): void { diff --git a/packages/cli-core/src/commands/init/index.test.ts b/packages/cli-core/src/commands/init/index.test.ts index a571d323a..42b7765db 100644 --- a/packages/cli-core/src/commands/init/index.test.ts +++ b/packages/cli-core/src/commands/init/index.test.ts @@ -1,143 +1,29 @@ -import { test, expect, describe, afterEach, spyOn } from "bun:test"; -import { useCaptureLog } from "../../test/lib/stubs.ts"; +import { test, expect, describe, spyOn } from "bun:test"; // Pure spyOn approach — Bun's mock.module globally replaces modules for the -// entire test run, which pollutes other test files (link, env/pull, config, -// context, etc.) that import the same modules. spyOn restores cleanly. -import * as loginMod from "../auth/login.ts"; -import * as linkMod from "../link/index.ts"; -import * as pullMod from "../env/pull.ts"; -import * as mode from "../../mode.ts"; -import * as config from "../../lib/config.ts"; -import * as frameworkMod from "../../lib/framework.ts"; -import * as context from "./context.ts"; -import * as scaffoldMod from "./scaffold.ts"; -import * as previewMod from "./preview.ts"; -import * as formatMod from "./format.ts"; -import * as scanMod from "./scan.ts"; -import * as heuristics from "./heuristics.ts"; -import * as skillsMod from "./skills.ts"; -import * as bootstrapMod from "./bootstrap.ts"; -import * as nextStepsMod from "../../lib/next-steps.ts"; -import * as keylessMod from "../../lib/keyless.ts"; +// entire test run, which pollutes other test files that import the same +// modules. spyOn restores cleanly. Shared setup lives in the harness. +import { + useInitHarness, + FAKE_CTX, + FAKE_BOOTSTRAP, + loginMod, + linkMod, + pullMod, + config, + frameworkMod, + context, + scaffoldMod, + previewMod, + heuristics, + skillsMod, + bootstrapMod, + nextStepsMod, +} from "../../test/lib/init-harness.ts"; import { init } from "./index.ts"; -const FAKE_CTX = { - cwd: "/tmp/test", - framework: { - dep: "react", - name: "React", - sdk: "@clerk/react", - envVar: "VITE_CLERK_PUBLISHABLE_KEY", - envFile: ".env" as const, - }, - typescript: true, - srcDir: false, - packageManager: "npm" as const, - existingClerk: true, - deps: { react: "^19.0.0" }, - envFile: ".env", -}; - -const FAKE_BOOTSTRAP = { - projectDir: "/tmp/test/my-app", - projectName: "my-app", - packageManager: "npm" as const, -}; - -type FakeFramework = { - dep: string; - name: string; - sdk: string; - envVar: string; - envFile: ".env" | ".env.local"; - supportsKeyless?: boolean; -}; - -type FakeCtx = Omit & { framework: FakeFramework }; - -const KEYLESS_CTX: FakeCtx = { - ...FAKE_CTX, - existingClerk: false, - framework: { ...FAKE_CTX.framework, supportsKeyless: true }, -}; - -function mockBootstrapTo(ctx: FakeCtx): void { - spyOn(context, "gatherContext").mockResolvedValueOnce(null).mockResolvedValueOnce(ctx); -} - -function mockExistingProject(ctx: FakeCtx): void { - spyOn(context, "gatherContext").mockResolvedValue(ctx); -} - -function mockMiddlewareScaffold(): void { - spyOn(scaffoldMod, "scaffold").mockResolvedValue({ - actions: [{ type: "create", path: "middleware.ts", content: "", description: "" }], - postInstructions: [], - }); -} - describe("init", () => { - let spies: ReturnType[]; - const captured = useCaptureLog(); - - afterEach(() => { - for (const s of spies) s.mockRestore(); - }); - - function setup(overrides: { email?: string | null; apiKey?: boolean; isAgent?: boolean } = {}) { - const email = overrides.email ?? null; - const apiKey = overrides.apiKey ?? false; - const agent = overrides.isAgent ?? false; - const authed = email != null || apiKey; - const gatherContextSpy = spyOn(context, "gatherContext").mockResolvedValue(null); - - spies = [ - spyOn(mode, "isAgent").mockReturnValue(agent), - spyOn(mode, "isHuman").mockReturnValue(!agent), - spyOn(config, "resolveProfile").mockResolvedValue(undefined), - spyOn(frameworkMod, "lookupFramework").mockReturnValue(null), - gatherContextSpy, - spyOn(context, "hasPackageJson").mockResolvedValue(false), - spyOn(scaffoldMod, "scaffold").mockResolvedValue({ actions: [], postInstructions: [] }), - spyOn(scaffoldMod, "enrichProjectContext").mockResolvedValue(undefined), - spyOn(previewMod, "previewPlan").mockReturnValue(undefined), - spyOn(previewMod, "previewAndConfirm").mockResolvedValue(true), - spyOn(formatMod, "runFormatters").mockResolvedValue(undefined), - spyOn(scanMod, "detectAuthLibraries").mockReturnValue(undefined), - spyOn(scanMod, "scanForIssues").mockResolvedValue([]), - spyOn(heuristics, "getAuthenticatedEmail").mockResolvedValue(email), - spyOn(heuristics, "isAuthenticated").mockResolvedValue(authed), - spyOn(heuristics, "printKeylessInfo").mockReturnValue(undefined), - spyOn(heuristics, "installSdk").mockResolvedValue(undefined), - spyOn(heuristics, "installDeps").mockResolvedValue(undefined), - spyOn(heuristics, "writePlan").mockResolvedValue([]), - spyOn(heuristics, "checkGitDirty").mockResolvedValue(false), - spyOn(heuristics, "printOutro").mockReturnValue(undefined), - spyOn(skillsMod, "installSkills").mockResolvedValue(undefined), - spyOn(loginMod, "login").mockResolvedValue(undefined as never), - spyOn(linkMod, "link").mockResolvedValue(undefined), - spyOn(pullMod, "pull").mockResolvedValue(undefined), - spyOn(bootstrapMod, "promptAndBootstrap").mockResolvedValue(FAKE_BOOTSTRAP), - spyOn(bootstrapMod, "confirmOverwrite").mockResolvedValue(undefined), - spyOn(keylessMod, "createAccountlessApp").mockResolvedValue({ - publishable_key: "pk_test_stub", - secret_key: "sk_test_stub", - claim_url: "/apps/claim?token=stub_token", - }), - spyOn(keylessMod, "writeKeysToEnvFile").mockResolvedValue(undefined), - spyOn(keylessMod, "writeKeylessBreadcrumb").mockResolvedValue(undefined), - ]; - - return { gatherContextSpy, captured }; - } - - function setupBootstrapSuccess() { - const gatherSpy = - spies.find((s) => s.getMockName?.() === "gatherContext") ?? spyOn(context, "gatherContext"); - gatherSpy.mockResolvedValueOnce(null).mockResolvedValueOnce(FAKE_CTX); - } - + const { setup, setupBootstrapSuccess, track } = useInitHarness(); test("suppresses auth next-steps when login runs during init", async () => { setup({ email: null }); spyOn(context, "gatherContext").mockResolvedValue(FAKE_CTX); @@ -263,12 +149,12 @@ describe("init", () => { }); const callOrder: string[] = []; - spies.push( + track( spyOn(skillsMod, "installSkills").mockImplementation(async () => { callOrder.push("installSkills"); }), ); - spies.push( + track( spyOn(nextStepsMod, "printNextSteps").mockImplementation(() => { callOrder.push("printNextSteps"); }), @@ -279,302 +165,6 @@ describe("init", () => { expect(callOrder.indexOf("installSkills")).toBeLessThan(callOrder.indexOf("printNextSteps")); }); - test("blank dir with keyless framework triggers login by default when unauthenticated", async () => { - setup(); - mockBootstrapTo(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({}); - - expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); - // Default flow now requires login; keyless is opt-in via --keyless. - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); - expect(linkMod.link).toHaveBeenCalled(); - }); - - test("--keyless on a keyless-capable framework uses keyless mode without logging in", async () => { - setup(); - mockBootstrapTo(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({ keyless: true }); - - expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); - expect(heuristics.printKeylessInfo).toHaveBeenCalled(); - expect(loginMod.login).not.toHaveBeenCalled(); - expect(linkMod.link).not.toHaveBeenCalled(); - expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); - }); - - test("--keyless takes precedence over an authed user", async () => { - setup({ email: "user@example.com" }); - mockBootstrapTo(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({ keyless: true }); - - expect(heuristics.printKeylessInfo).toHaveBeenCalled(); - expect(linkMod.link).not.toHaveBeenCalled(); - expect(pullMod.pull).not.toHaveBeenCalled(); - }); - - test("--keyless on a non-keyless framework throws a usage error", async () => { - setup(); - const nonKeylessCtx: FakeCtx = { - ...FAKE_CTX, - existingClerk: false, - framework: { - dep: "vue", - name: "Vue", - sdk: "@clerk/vue", - envVar: "VITE_CLERK_PUBLISHABLE_KEY", - envFile: ".env.local", - }, - envFile: ".env.local", - }; - mockBootstrapTo(nonKeylessCtx); - - await expect(init({ keyless: true })).rejects.toThrow(/--keyless is not supported for Vue/); - expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); - expect(linkMod.link).not.toHaveBeenCalled(); - }); - - test("--keyless on an existing keyless-capable project uses keyless mode", async () => { - setup(); - mockExistingProject(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({ keyless: true }); - - expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); - expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); - expect(heuristics.printKeylessInfo).toHaveBeenCalled(); - expect(linkMod.link).not.toHaveBeenCalled(); - expect(loginMod.login).not.toHaveBeenCalled(); - }); - - test("bootstrap with keyless framework goes authenticated when already signed in", async () => { - setup({ email: "user@example.com" }); - mockBootstrapTo({ ...KEYLESS_CTX, existingClerk: true }); - - await init({}); - - expect(heuristics.isAuthenticated).toHaveBeenCalled(); - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - expect(linkMod.link).toHaveBeenCalled(); - }); - - test("-y flag with keyless framework uses authenticated flow when signed in", async () => { - setup({ email: "user@example.com" }); - mockBootstrapTo({ ...KEYLESS_CTX, existingClerk: true }); - - await init({ yes: true }); - - expect(heuristics.isAuthenticated).toHaveBeenCalled(); - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - }); - - test("-y flag with keyless framework uses authenticated flow when CLERK_PLATFORM_API_KEY is set", async () => { - setup({ apiKey: true }); - mockBootstrapTo({ ...KEYLESS_CTX, existingClerk: true }); - - await init({ yes: true }); - - expect(heuristics.isAuthenticated).toHaveBeenCalled(); - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - expect(linkMod.link).toHaveBeenCalled(); - }); - - test("-y flag with keyless framework triggers login when unauthenticated (no --keyless)", async () => { - // `-y` skips y/n confirmations but does not skip authentication. Without - // `--keyless`, init must prompt the user to log in. - setup(); - mockBootstrapTo(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({ yes: true }); - - expect(heuristics.isAuthenticated).toHaveBeenCalled(); - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); - expect(linkMod.link).toHaveBeenCalled(); - }); - - test("-y --keyless with keyless framework uses keyless mode", async () => { - setup(); - mockBootstrapTo(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({ yes: true, keyless: true }); - - expect(heuristics.printKeylessInfo).toHaveBeenCalled(); - expect(linkMod.link).not.toHaveBeenCalled(); - expect(loginMod.login).not.toHaveBeenCalled(); - }); - - test("agent mode with keyless framework prints manual setup when unauthenticated", async () => { - // Agents can't run interactive OAuth and didn't opt into keyless via - // --keyless, so the safe path is to scaffold locally and emit guidance. - const { captured } = setup({ isAgent: true, email: null }); - mockExistingProject(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({}); - - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - expect(linkMod.link).not.toHaveBeenCalled(); - expect(pullMod.pull).not.toHaveBeenCalled(); - expect(loginMod.login).not.toHaveBeenCalled(); - expect(captured.err).toContain("clerk init --keyless"); - }); - - test("agent mode with --keyless uses keyless mode without authentication", async () => { - setup({ isAgent: true, email: null }); - mockExistingProject(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({ keyless: true }); - - expect(heuristics.printKeylessInfo).toHaveBeenCalled(); - expect(linkMod.link).not.toHaveBeenCalled(); - expect(loginMod.login).not.toHaveBeenCalled(); - expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); - }); - - test("agent mode with keyless framework + authed creates and links a real app", async () => { - setup({ isAgent: true, email: "user@example.com" }); - mockExistingProject(KEYLESS_CTX); - // Override potential leakage from earlier tests that spy on resolveProfile - // with a non-undefined value but don't track those spies for restoration. - spyOn(config, "resolveProfile").mockResolvedValue(undefined); - mockMiddlewareScaffold(); - - await init({}); - - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - expect(linkMod.link).toHaveBeenCalledWith({ - skipIfLinked: true, - app: undefined, - cwd: KEYLESS_CTX.cwd, - createIfMissing: expect.any(String), - }); - expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: KEYLESS_CTX.cwd }); - }); - - test("agent mode with keyless framework uses linked profile as a real app target", async () => { - setup({ isAgent: true, email: "user@example.com" }); - mockExistingProject(KEYLESS_CTX); - spyOn(config, "resolveProfile").mockResolvedValue({ - profile: { appId: "app_123" }, - } as never); - mockMiddlewareScaffold(); - - await init({}); - - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - expect(linkMod.link).not.toHaveBeenCalled(); - expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: KEYLESS_CTX.cwd }); - }); - - test("agent mode with keyless framework and --app uses real app flow", async () => { - setup({ isAgent: true, email: "user@example.com" }); - mockExistingProject(KEYLESS_CTX); - mockMiddlewareScaffold(); - - await init({ app: "app_abc" }); - - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - expect(linkMod.link).toHaveBeenCalledWith({ - skipIfLinked: true, - app: "app_abc", - cwd: KEYLESS_CTX.cwd, - createIfMissing: expect.any(String), - }); - expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: KEYLESS_CTX.cwd }); - }); - - test("agent mode with non-keyless framework and no app target prints manual setup", async () => { - const { captured } = setup({ isAgent: true, email: "user@example.com" }); - - const noKeylessCtx = { - ...FAKE_CTX, - existingClerk: false, - framework: { - dep: "vue", - name: "Vue", - sdk: "@clerk/vue", - envVar: "VITE_CLERK_PUBLISHABLE_KEY", - envFile: ".env.local" as const, - }, - envFile: ".env.local", - }; - spyOn(context, "gatherContext").mockResolvedValue(noKeylessCtx); - spyOn(scaffoldMod, "scaffold").mockResolvedValue({ - actions: [{ type: "create", path: "src/main.ts", content: "", description: "" }], - postInstructions: [], - }); - - await init({}); - - expect(linkMod.link).not.toHaveBeenCalled(); - expect(pullMod.pull).not.toHaveBeenCalled(); - expect(loginMod.login).not.toHaveBeenCalled(); - expect(captured.err).toContain("clerk init --app "); - }); - - test("agent mode with real app target and no auth launches login", async () => { - setup({ isAgent: true }); - spyOn(context, "gatherContext").mockResolvedValue(FAKE_CTX); - - await init({ app: "app_abc" }); - - expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); - expect(linkMod.link).toHaveBeenCalledWith({ - skipIfLinked: true, - app: "app_abc", - cwd: FAKE_CTX.cwd, - createIfMissing: expect.any(String), - }); - }); - - test("-y flag triggers login when unauthenticated", async () => { - setup(); - setupBootstrapSuccess(); - - await init({ yes: true }); - - expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); - expect(heuristics.isAuthenticated).toHaveBeenCalled(); - // `-y` skips y/n confirmations but not authentication. - expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); - }); - - test("-y flag triggers login for non-keyless frameworks in bootstrap", async () => { - setup(); - - const noKeylessCtx = { - ...FAKE_CTX, - framework: { - dep: "vue", - name: "Vue", - sdk: "@clerk/vue", - envVar: "VITE_CLERK_PUBLISHABLE_KEY", - envFile: ".env.local" as const, - }, - existingClerk: false, - }; - - spyOn(context, "gatherContext").mockResolvedValueOnce(null).mockResolvedValueOnce(noKeylessCtx); - - await init({ yes: true }); - - expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); - expect(heuristics.isAuthenticated).toHaveBeenCalled(); - expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - }); - test("blank dir bootstrap declined throws UserAbortError", async () => { setup(); spyOn(bootstrapMod, "promptAndBootstrap").mockRejectedValue( @@ -606,56 +196,6 @@ describe("init", () => { expect(context.hasPackageJson).not.toHaveBeenCalled(); }); - test("existing repo with keyless framework uses authenticated flow when signed in", async () => { - setup({ email: "user@example.com" }); - - const keylessCtx = { - ...FAKE_CTX, - framework: { ...FAKE_CTX.framework, supportsKeyless: true }, - }; - spyOn(context, "gatherContext").mockResolvedValue(keylessCtx); - spyOn(config, "resolveProfile").mockResolvedValue({ profile: { appId: "app_123" } } as never); - - await init({ yes: true }); - - expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); - expect(heuristics.isAuthenticated).toHaveBeenCalled(); - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - }); - - test("existing repo with keyless framework uses authenticated flow when not signed in", async () => { - // Keyless auto-selection is scoped to bootstrap (new-project) flows. On an - // existing repo, an unauthenticated re-run should fall through to the - // authenticated flow (which prompts login) rather than silently skip - // `env pull`. - setup(); - - const keylessCtx = { - ...FAKE_CTX, - existingClerk: false, - framework: { ...FAKE_CTX.framework, supportsKeyless: true }, - }; - spyOn(context, "gatherContext").mockResolvedValue(keylessCtx); - spyOn(scaffoldMod, "scaffold").mockResolvedValue({ - actions: [{ type: "create", path: "middleware.ts", content: "", description: "" }], - postInstructions: [], - }); - spyOn(loginMod, "login").mockResolvedValue({ - userId: "user_1", - email: "test@test.com", - } as never); - - await init({}); - - expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); - expect(heuristics.isAuthenticated).toHaveBeenCalled(); - expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); - // Unauthenticated + existing repo → login + link run via authenticateAndLink. - expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); - expect(linkMod.link).toHaveBeenCalled(); - expect(pullMod.pull).toHaveBeenCalled(); - }); - test("passes frameworkOverride to bootstrap when provided", async () => { const fwOverride = { dep: "next", diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index d358a2992..e2b46c920 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -20,6 +20,8 @@ import { writeKeysToEnvFile, parseClaimToken, writeKeylessBreadcrumb, + KEYLESS_TEMPLATES, + type KeylessTemplate, } from "../../lib/keyless.js"; import { printNextSteps } from "../../lib/next-steps.js"; import { gatherContext, hasPackageJson } from "./context.js"; @@ -60,14 +62,20 @@ type InitOptions = { starter?: boolean; /** Link to a specific Clerk application by ID (skips the interactive picker). */ app?: string; - /** Opt into keyless mode (auto-generated dev keys, no login). Only valid on keyless-capable frameworks. */ + /** Force keyless mode (auto-generated dev keys, no login). Only valid on keyless-capable frameworks. */ keyless?: boolean; + /** Force the authenticated flow (log in and link a real app) instead of defaulting to keyless. */ + login?: boolean; + /** Pre-configure the keyless application from a Clerk application template. */ + template?: KeylessTemplate; }; export async function init(options: InitOptions = {}) { const cwd = process.cwd(); const agent = isAgent(); + await assertUsableFlags(options, agent); + const frameworkOverride = options.framework ? (lookupFramework(options.framework) ?? undefined) : undefined; @@ -105,8 +113,10 @@ export async function init(options: InitOptions = {}) { const strategy = pickStrategy({ optsKeyless, + optsLogin: options.login === true, agent, authed, + isBootstrap: bootstrap != null, hasRealAppTarget, framework: ctx.framework, }); @@ -136,7 +146,7 @@ export async function init(options: InitOptions = {}) { } bar(); - await runStrategy(strategy, ctx); + await runStrategy(strategy, ctx, options.template); // Native platforms (iOS/Android) have no npx/Node toolchain to run `skills add` with. if (options.skills !== false && isNpmFramework(ctx.framework)) { @@ -153,6 +163,32 @@ export async function init(options: InitOptions = {}) { outro("Done"); } +/** + * Rejects flag combinations that can't both be honoured, before anything is + * bootstrapped on disk. `--keyless` and `--template` describe an application the + * CLI creates; `--login` and `--app` describe one that already exists. + */ +async function assertUsableFlags(options: InitOptions, agent: boolean): Promise { + if (options.keyless && options.login) { + throwUsageError("--keyless and --login cannot be combined."); + } + if (options.keyless && options.app) { + throwUsageError( + "--keyless cannot be combined with --app. Drop --keyless to link the app, or drop --app to use temporary development keys.", + ); + } + if (options.template && options.login) { + throwUsageError( + "--template applies to keyless applications and cannot be combined with --login.", + ); + } + if (options.login && agent && !(await isAuthenticated())) { + throwUsageError( + "--login requires an interactive terminal to complete the browser login. Ask the user to run `clerk auth login`, then re-run `clerk init`.", + ); + } +} + type ResolvedContext = { ctx: ProjectContext; bootstrap: BootstrapResult | null; @@ -237,19 +273,14 @@ function printBootstrapNextSteps( } function printBootstrapManualSetupInfo(framework: FrameworkInfo): void { - const lines = [`\n Set up Clerk for ${framework.name}:`]; - if (framework.supportsKeyless) { - lines.push( - " clerk init --keyless (use temporary development keys)", - " clerk auth login (then re-run clerk init to link a real app)", - ); - } else { - lines.push( - ` ${framework.name} requires API keys — set them up manually:`, - " clerk init --app ", - " clerk env pull", - ); - } + // Only reachable for non-keyless frameworks: keyless-capable ones resolve to + // the "keyless" or "authenticate" strategy in agent mode instead. + const lines = [ + `\n Set up Clerk for ${framework.name}:`, + ` ${framework.name} requires API keys — set them up manually:`, + " clerk init --app ", + " clerk env pull", + ]; log.info(lines.map(dim).join("\n")); } @@ -258,20 +289,28 @@ function printBootstrapManualSetupInfo(framework: FrameworkInfo): void { type InitStrategy = "keyless" | "manual" | "authenticate"; // Picks how `clerk init` will reach a working Clerk setup: -// - "keyless" → user opted in via `--keyless`; needs a keyless-capable framework (else: usage error). -// - "manual" → agent mode can't auto-resolve (no real app target, plus either a non-keyless framework -// or no auth) — scaffold locally and print guidance instead of running OAuth. -// - "authenticate" → default; log in (interactively if needed) and link a real Clerk application. +// - "keyless" → temporary development keys, no login. Forced via `--keyless`, or the default +// for unauthenticated runs on a keyless-capable framework (human bootstrap and +// all agent runs). A `.clerk/keyless.json` breadcrumb lets the next +// `clerk auth login` claim the app automatically. +// - "manual" → agent mode on a non-keyless framework without a real app target — scaffold +// locally and print guidance instead of running OAuth. +// - "authenticate" → log in (interactively if needed) and link a real Clerk application. Forced +// via `--login`, and the default whenever keyless doesn't apply. function pickStrategy({ optsKeyless, + optsLogin, agent, authed, + isBootstrap, hasRealAppTarget, framework, }: { optsKeyless: boolean; + optsLogin: boolean; agent: boolean; authed: boolean; + isBootstrap: boolean; hasRealAppTarget: boolean; framework: FrameworkInfo; }): InitStrategy { @@ -283,11 +322,17 @@ function pickStrategy({ } return "keyless"; } - if (agent && !hasRealAppTarget && (!framework.supportsKeyless || !authed)) return "manual"; + if (optsLogin || hasRealAppTarget) return "authenticate"; + if (agent && !framework.supportsKeyless) return "manual"; + if (!authed && framework.supportsKeyless && (agent || isBootstrap)) return "keyless"; return "authenticate"; } -async function runStrategy(strategy: InitStrategy, ctx: ProjectContext): Promise { +async function runStrategy( + strategy: InitStrategy, + ctx: ProjectContext, + template?: KeylessTemplate, +): Promise { switch (strategy) { case "manual": printBootstrapManualSetupInfo(ctx.framework); @@ -296,7 +341,7 @@ async function runStrategy(strategy: InitStrategy, ctx: ProjectContext): Promise await pull({ file: ctx.envFile, cwd: ctx.cwd }); return; case "keyless": - await setupKeylessApp(ctx.cwd, ctx.framework.dep, ctx.envFile); + await setupKeylessApp(ctx.cwd, ctx.framework.dep, ctx.envFile, template); return; } } @@ -338,10 +383,18 @@ async function authenticateAndLink( // --- Keyless app setup --- -async function setupKeylessApp(cwd: string, frameworkDep: string, envFile: string): Promise { +async function setupKeylessApp( + cwd: string, + frameworkDep: string, + envFile: string, + template?: KeylessTemplate, +): Promise { try { - const app = await withSpinner("Creating development application...", () => - createAccountlessApp(frameworkDep), + const app = await withSpinner( + template + ? `Creating development application (${template})...` + : "Creating development application...", + () => createAccountlessApp(frameworkDep, template), ); await writeKeysToEnvFile(cwd, { @@ -455,7 +508,17 @@ export function registerInit(program: Program): void { .option("--starter", "Create a new project from a starter template") .option( "--keyless", - "Use keyless development keys instead of logging in (only for keyless-capable frameworks)", + "Force keyless development keys, even when logged in (only for keyless-capable frameworks)", + ) + .option( + "--login", + "Force the authenticated flow: log in and link a real application instead of keyless keys", + ) + .addOption( + createOption( + "--template ", + "Pre-configure the keyless application from a Clerk application template", + ).choices(KEYLESS_TEMPLATES), ) .option("-y, --yes", "Skip confirmation prompts") .option("--no-skills", "Skip the optional agent skills install prompt") @@ -476,7 +539,15 @@ export function registerInit(program: Program): void { }, { command: "clerk init --starter --framework next --keyless", - description: "Bootstrap without logging in (uses temporary dev keys)", + description: "Bootstrap with temporary dev keys, even when logged in", + }, + { + command: "clerk init --login", + description: "Log in and link a real application instead of keyless keys", + }, + { + command: "clerk init --template b2b-saas", + description: "Bootstrap a keyless app pre-configured for B2B SaaS", }, { command: "clerk init -y", description: "Skip all confirmation prompts" }, { command: "clerk init --no-skills", description: "Skip the agent skills install prompt" }, diff --git a/packages/cli-core/src/commands/init/strategy.test.ts b/packages/cli-core/src/commands/init/strategy.test.ts new file mode 100644 index 000000000..a1a93079d --- /dev/null +++ b/packages/cli-core/src/commands/init/strategy.test.ts @@ -0,0 +1,455 @@ +import { test, expect, describe, spyOn } from "bun:test"; + +// Pure spyOn approach — Bun's mock.module globally replaces modules for the +// entire test run, which pollutes other test files that import the same +// modules. spyOn restores cleanly. Shared setup lives in the harness. +import { + useInitHarness, + FAKE_CTX, + KEYLESS_CTX, + mockBootstrapTo, + mockExistingProject, + mockMiddlewareScaffold, + type FakeCtx, + loginMod, + linkMod, + pullMod, + config, + context, + scaffoldMod, + heuristics, + bootstrapMod, + keylessMod, +} from "../../test/lib/init-harness.ts"; +import { init } from "./index.ts"; + +describe("init strategy", () => { + const { setup, setupBootstrapSuccess } = useInitHarness(); + test("blank dir with keyless framework defaults to keyless when unauthenticated", async () => { + setup(); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({}); + + expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); + // Keyless is the default for unauthenticated bootstrap; login is opt-in via --login. + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + expect(keylessMod.writeKeylessBreadcrumb).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + }); + + test("--login on unauthenticated bootstrap forces the authenticated flow", async () => { + setup(); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ login: true }); + + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); + expect(linkMod.link).toHaveBeenCalled(); + }); + + test("--template is forwarded to the keyless application create call", async () => { + setup(); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ template: "b2b-saas" }); + + expect(keylessMod.createAccountlessApp).toHaveBeenCalledWith( + KEYLESS_CTX.framework.dep, + "b2b-saas", + ); + }); + + test("--template with --login throws a usage error", async () => { + setup(); + + await expect(init({ template: "b2b-saas", login: true })).rejects.toThrow( + /--template applies to keyless applications/, + ); + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + }); + + test("--keyless with --login throws a usage error before bootstrapping", async () => { + setup(); + + await expect(init({ keyless: true, login: true })).rejects.toThrow( + /--keyless and --login cannot be combined/, + ); + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + }); + + test("--keyless with --app throws a usage error before bootstrapping", async () => { + setup(); + + await expect(init({ keyless: true, app: "app_abc" })).rejects.toThrow( + /--keyless cannot be combined with --app/, + ); + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + }); + + test("--keyless on a keyless-capable framework uses keyless mode without logging in", async () => { + setup(); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ keyless: true }); + + expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + }); + + test("--keyless takes precedence over an authed user", async () => { + setup({ email: "user@example.com" }); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ keyless: true }); + + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(pullMod.pull).not.toHaveBeenCalled(); + }); + + test("--keyless on a non-keyless framework throws a usage error", async () => { + setup(); + const nonKeylessCtx: FakeCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { + dep: "vue", + name: "Vue", + sdk: "@clerk/vue", + envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env.local", + }, + envFile: ".env.local", + }; + mockBootstrapTo(nonKeylessCtx); + + await expect(init({ keyless: true })).rejects.toThrow(/--keyless is not supported for Vue/); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + }); + + test("--keyless on an existing keyless-capable project uses keyless mode", async () => { + setup(); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ keyless: true }); + + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + }); + + test("bootstrap with keyless framework goes authenticated when already signed in", async () => { + setup({ email: "user@example.com" }); + mockBootstrapTo({ ...KEYLESS_CTX, existingClerk: true }); + + await init({}); + + expect(heuristics.isAuthenticated).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).toHaveBeenCalled(); + }); + + test("-y flag with keyless framework uses authenticated flow when signed in", async () => { + setup({ email: "user@example.com" }); + mockBootstrapTo({ ...KEYLESS_CTX, existingClerk: true }); + + await init({ yes: true }); + + expect(heuristics.isAuthenticated).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + }); + + test("-y flag with keyless framework uses authenticated flow when CLERK_PLATFORM_API_KEY is set", async () => { + setup({ apiKey: true }); + mockBootstrapTo({ ...KEYLESS_CTX, existingClerk: true }); + + await init({ yes: true }); + + expect(heuristics.isAuthenticated).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).toHaveBeenCalled(); + }); + + test("-y flag with keyless framework stays keyless when unauthenticated", async () => { + // `-y` only skips y/n confirmations — it neither forces nor bypasses the + // keyless default for unauthenticated bootstrap. + setup(); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ yes: true }); + + expect(heuristics.isAuthenticated).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + }); + + test("-y --keyless with keyless framework uses keyless mode", async () => { + setup(); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ yes: true, keyless: true }); + + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + }); + + test("agent mode with keyless framework uses keyless with breadcrumb when unauthenticated", async () => { + // Agents can't run interactive OAuth, so unauthenticated agent runs default + // to keyless: the app works immediately and the breadcrumb lets the next + // `clerk auth login` claim it. + setup({ isAgent: true, email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({}); + + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + expect(keylessMod.writeKeylessBreadcrumb).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(pullMod.pull).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + }); + + test("agent mode with --login while unauthenticated throws a usage error", async () => { + setup({ isAgent: true, email: null }); + + await expect(init({ login: true })).rejects.toThrow(/--login requires an interactive terminal/); + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + }); + + test("agent mode with --login while authenticated runs the authenticated flow", async () => { + setup({ isAgent: true, email: "user@example.com" }); + mockExistingProject(KEYLESS_CTX); + spyOn(config, "resolveProfile").mockResolvedValue(undefined); + mockMiddlewareScaffold(); + + await init({ login: true }); + + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).toHaveBeenCalled(); + expect(pullMod.pull).toHaveBeenCalled(); + }); + + test("agent mode with --keyless uses keyless mode without authentication", async () => { + setup({ isAgent: true, email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ keyless: true }); + + expect(heuristics.printKeylessInfo).toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + }); + + test("agent mode with keyless framework + authed creates and links a real app", async () => { + setup({ isAgent: true, email: "user@example.com" }); + mockExistingProject(KEYLESS_CTX); + // Override potential leakage from earlier tests that spy on resolveProfile + // with a non-undefined value but don't track those spies for restoration. + spyOn(config, "resolveProfile").mockResolvedValue(undefined); + mockMiddlewareScaffold(); + + await init({}); + + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).toHaveBeenCalledWith({ + skipIfLinked: true, + app: undefined, + cwd: KEYLESS_CTX.cwd, + createIfMissing: expect.any(String), + }); + expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: KEYLESS_CTX.cwd }); + }); + + test("agent mode with keyless framework uses linked profile as a real app target", async () => { + setup({ isAgent: true, email: "user@example.com" }); + mockExistingProject(KEYLESS_CTX); + spyOn(config, "resolveProfile").mockResolvedValue({ + profile: { appId: "app_123" }, + } as never); + mockMiddlewareScaffold(); + + await init({}); + + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).not.toHaveBeenCalled(); + expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: KEYLESS_CTX.cwd }); + }); + + test("agent mode with keyless framework and --app uses real app flow", async () => { + setup({ isAgent: true, email: "user@example.com" }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ app: "app_abc" }); + + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + expect(linkMod.link).toHaveBeenCalledWith({ + skipIfLinked: true, + app: "app_abc", + cwd: KEYLESS_CTX.cwd, + createIfMissing: expect.any(String), + }); + expect(pullMod.pull).toHaveBeenCalledWith({ file: ".env", cwd: KEYLESS_CTX.cwd }); + }); + + test("agent mode with non-keyless framework and no app target prints manual setup", async () => { + const { captured } = setup({ isAgent: true, email: "user@example.com" }); + + const noKeylessCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { + dep: "vue", + name: "Vue", + sdk: "@clerk/vue", + envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env.local" as const, + }, + envFile: ".env.local", + }; + spyOn(context, "gatherContext").mockResolvedValue(noKeylessCtx); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "src/main.ts", content: "", description: "" }], + postInstructions: [], + }); + + await init({}); + + expect(linkMod.link).not.toHaveBeenCalled(); + expect(pullMod.pull).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + expect(captured.err).toContain("clerk init --app "); + }); + + test("agent mode with real app target and no auth launches login", async () => { + setup({ isAgent: true }); + spyOn(context, "gatherContext").mockResolvedValue(FAKE_CTX); + + await init({ app: "app_abc" }); + + expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); + expect(linkMod.link).toHaveBeenCalledWith({ + skipIfLinked: true, + app: "app_abc", + cwd: FAKE_CTX.cwd, + createIfMissing: expect.any(String), + }); + }); + + test("-y flag triggers login when unauthenticated", async () => { + setup(); + setupBootstrapSuccess(); + + await init({ yes: true }); + + expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); + expect(heuristics.isAuthenticated).toHaveBeenCalled(); + // `-y` skips y/n confirmations but not authentication. + expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); + }); + + test("-y flag triggers login for non-keyless frameworks in bootstrap", async () => { + setup(); + + const noKeylessCtx = { + ...FAKE_CTX, + framework: { + dep: "vue", + name: "Vue", + sdk: "@clerk/vue", + envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env.local" as const, + }, + existingClerk: false, + }; + + spyOn(context, "gatherContext").mockResolvedValueOnce(null).mockResolvedValueOnce(noKeylessCtx); + + await init({ yes: true }); + + expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); + expect(heuristics.isAuthenticated).toHaveBeenCalled(); + expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + }); + test("existing repo with keyless framework uses authenticated flow when signed in", async () => { + setup({ email: "user@example.com" }); + + const keylessCtx = { + ...FAKE_CTX, + framework: { ...FAKE_CTX.framework, supportsKeyless: true }, + }; + spyOn(context, "gatherContext").mockResolvedValue(keylessCtx); + spyOn(config, "resolveProfile").mockResolvedValue({ profile: { appId: "app_123" } } as never); + + await init({ yes: true }); + + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + expect(heuristics.isAuthenticated).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + }); + + test("existing repo with keyless framework uses authenticated flow when not signed in", async () => { + // Keyless auto-selection is scoped to bootstrap (new-project) flows. On an + // existing repo, an unauthenticated re-run should fall through to the + // authenticated flow (which prompts login) rather than silently skip + // `env pull`. + setup(); + + const keylessCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { ...FAKE_CTX.framework, supportsKeyless: true }, + }; + spyOn(context, "gatherContext").mockResolvedValue(keylessCtx); + spyOn(scaffoldMod, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "middleware.ts", content: "", description: "" }], + postInstructions: [], + }); + spyOn(loginMod, "login").mockResolvedValue({ + userId: "user_1", + email: "test@test.com", + } as never); + + await init({}); + + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + expect(heuristics.isAuthenticated).toHaveBeenCalled(); + expect(heuristics.printKeylessInfo).not.toHaveBeenCalled(); + // Unauthenticated + existing repo → login + link run via authenticateAndLink. + expect(loginMod.login).toHaveBeenCalledWith({ showNextSteps: false }); + expect(linkMod.link).toHaveBeenCalled(); + expect(pullMod.pull).toHaveBeenCalled(); + }); +}); diff --git a/packages/cli-core/src/commands/orgs/README.md b/packages/cli-core/src/commands/orgs/README.md index f585f9c8d..41f8f7ca9 100644 --- a/packages/cli-core/src/commands/orgs/README.md +++ b/packages/cli-core/src/commands/orgs/README.md @@ -4,6 +4,17 @@ Toggle Clerk Organizations on the linked instance. The handlers are wired to top-level `clerk enable orgs` and `clerk disable orgs` commands; the source lives here so future org-related commands (settings, CRUD) can co-locate. +Works without an account. When the directory isn't linked and no `--app` is +passed, both commands target the unclaimed keyless application whose secret key +the project holds locally, writing through `PATCH /v1/instance/organization_settings` +instead of the account-level config document. The payload field names are +identical on both paths, so nothing is translated. See +[keyless mode](../config/README.md#keyless-mode). + +One difference in keyless mode: `clerk disable orgs` can't run its +organization-billing pre-flight check, because billing is not readable without +an account. + ## Usage ``` diff --git a/packages/cli-core/src/commands/orgs/index.ts b/packages/cli-core/src/commands/orgs/index.ts index aebf8076c..ee495ac4d 100644 --- a/packages/cli-core/src/commands/orgs/index.ts +++ b/packages/cli-core/src/commands/orgs/index.ts @@ -1,10 +1,10 @@ -import { resolveAppContext } from "../../lib/config.ts"; import { fetchInstanceConfig } from "../../lib/plapi.ts"; import { throwUsageError, withApiContext } from "../../lib/errors.ts"; import { withGutter, withSpinner } from "../../lib/spinner.ts"; import { isHuman } from "../../mode.ts"; import { NEXT_STEPS } from "../../lib/next-steps.ts"; import { applyConfigPatch } from "../config/apply-patch.ts"; +import { resolveInstanceTarget } from "../../lib/keyless-target.ts"; interface OrgsOptions { app?: string; @@ -31,7 +31,7 @@ function parsePositiveInt(value: string, flag: string): number { } export async function orgsEnable(options: OrgsOptions): Promise { - const ctx = await resolveAppContext(options); + const target = await resolveInstanceTarget(options); const orgSettings: Record = { enabled: true }; if (options.forceSelection) orgSettings.force_organization_selection = true; @@ -53,7 +53,7 @@ export async function orgsEnable(options: OrgsOptions): Promise { await withGutter("Enabling organizations", async ({ setNextSteps }) => { const applied = await applyConfigPatch({ - ctx, + target, payload: { organization_settings: orgSettings }, verb: "Enabling organizations", successMessage: "Organizations enabled", @@ -69,17 +69,26 @@ export async function orgsEnable(options: OrgsOptions): Promise { } export async function orgsDisable(options: OrgsOptions): Promise { - const ctx = await resolveAppContext(options); + const target = await resolveInstanceTarget(options); await withGutter("Disabling organizations", async () => { - const current = await withSpinner("Fetching current config...", () => - withApiContext( - fetchInstanceConfig(ctx.appId, ctx.instanceId, ["billing", "organization_settings"]), - "Failed to fetch config", - ), - ); + // Billing lives only in the account-level config document, so a keyless run + // can't read it. Skip the pre-flight fetch entirely rather than half-run it: + // applyConfigPatch reads the group it needs on its own. + const current = + target.kind === "account" + ? await withSpinner("Fetching current config...", () => + withApiContext( + fetchInstanceConfig(target.ctx.appId, target.ctx.instanceId, [ + "billing", + "organization_settings", + ]), + "Failed to fetch config", + ), + ) + : undefined; - const billing = current.billing as Record | undefined; + const billing = current?.billing as Record | undefined; const orgBillingOn = billing?.organization_enabled === true; // Agent mode: refuse rather than warn-then-mutate (warn-then-mutate in CI @@ -92,7 +101,7 @@ export async function orgsDisable(options: OrgsOptions): Promise { } await applyConfigPatch({ - ctx, + target, payload: { organization_settings: { enabled: false } }, verb: "Disabling organizations", successMessage: "Organizations disabled", diff --git a/packages/cli-core/src/commands/whoami/README.md b/packages/cli-core/src/commands/whoami/README.md index b19098699..995ed0073 100644 --- a/packages/cli-core/src/commands/whoami/README.md +++ b/packages/cli-core/src/commands/whoami/README.md @@ -22,7 +22,31 @@ clerk whoami --json - Calls `resolveProfile(cwd)` (best-effort — failures are swallowed) to determine whether the working directory is linked to a Clerk application. - When linked, prints a `Linked to ...` line on **stderr** above the next-steps, where `...` is the app label rendered by `profileLabel()` from `lib/config.ts` — for example, `Linked to MyApp (app_xxx)`. - When not linked, only the existing `WHOAMI` next-steps are printed. -- If no token exists, throws an `AuthError` ("Not logged in"). +- If no token exists, falls back to the keyless path below; when that finds nothing either, throws an `AuthError` ("Not logged in"). + +### Keyless applications + +An unclaimed keyless application has no account to name, so the instance itself is the identity. When there's no stored token but the directory holds an instance secret key (env var, `.env`/`.env.local`, or `.clerk/.tmp/keyless.json` — see [`lib/keyless-target.ts`](../../lib/keyless-target.ts)), `whoami` reads `GET /v1/instance` with that key and reports the instance instead of erroring. + +```json +{ + "email": null, + "keyless": { + "instanceId": "ins_...", + "environmentType": "development", + "publishableKey": "pk_test_...", + "keySource": ".clerk/.tmp/keyless.json" + }, + "linked": null +} +``` + +In human mode the instance ID goes to **stdout** and the explanation (including where the key came from) to **stderr**. + +| Method | Endpoint | Description | +| ------ | -------------- | ------------------------------------------------------------------------- | +| `GET` | `/v1/instance` | Reads the keyless instance's identity. Authenticated with the secret key. | + - If the token is expired or invalid, throws an `AuthError` ("Session expired"). ### `--json` (and agent mode) diff --git a/packages/cli-core/src/commands/whoami/index.ts b/packages/cli-core/src/commands/whoami/index.ts index b9eeac76d..2b3de6401 100644 --- a/packages/cli-core/src/commands/whoami/index.ts +++ b/packages/cli-core/src/commands/whoami/index.ts @@ -7,15 +7,52 @@ import { AuthError } from "../../lib/errors.ts"; import { profileLabel, resolveProfile } from "../../lib/config.ts"; import { NEXT_STEPS, printNextSteps } from "../../lib/next-steps.ts"; import { isAgent } from "../../mode.ts"; +import { bapiRequest } from "../../lib/bapi.ts"; +import { + findLocalPublishableKey, + resolveKeylessTarget, + type KeylessTarget, +} from "../../lib/keyless-target.ts"; export interface WhoamiOptions { json?: boolean; } +/** + * Who this directory is acting as: a Clerk account, or — with no account at all + * — the unclaimed keyless application whose key the project holds. Resolving + * this first keeps rendering to a single place. + */ +type Identity = + | { kind: "account"; email: string; profile: Awaited> } + | { + kind: "keyless"; + instanceId: string | null; + environmentType: string | null; + publishableKey: string | null; + keySource: string; + }; + export async function whoami(options: WhoamiOptions = {}) { + const identity = await resolveIdentity(); + + if (options.json || isAgent()) { + log.data(JSON.stringify(toJson(identity), null, 2)); + return; + } + + render(identity); +} + +async function resolveIdentity(): Promise { const token = await getValidToken(); + + // No account, but the directory may still hold a working keyless + // application. Report what it is instead of a flat "not logged in". if (!token) { - throw new AuthError({ reason: "not_logged_in" }); + const keyless = await resolveKeylessTarget({ cwd: process.cwd() }); + if (!keyless) throw new AuthError({ reason: "not_logged_in" }); + return describeKeyless(keyless); } let userInfo; @@ -25,44 +62,75 @@ export async function whoami(options: WhoamiOptions = {}) { throw new AuthError({ reason: "session_expired" }); } - let resolved: Awaited>; + let profile: Awaited>; try { - resolved = await resolveProfile(process.cwd()); + profile = await resolveProfile(process.cwd()); } catch { // Best-effort only: don't fail whoami when local profile resolution fails. - resolved = undefined; + profile = undefined; } - if (options.json || isAgent()) { - log.data( - JSON.stringify( - { - email: userInfo.email, - linked: resolved - ? { - appId: resolved.profile.appId, - appName: resolved.profile.appName ?? null, - instances: { - development: resolved.profile.instances.development, - production: resolved.profile.instances.production ?? null, - }, - resolvedVia: resolved.resolvedVia, - path: resolved.path, - } - : null, - }, - null, - 2, - ), + return { kind: "account", email: userInfo.email, profile }; +} + +async function describeKeyless(keyless: KeylessTarget): Promise { + const instance = await withSpinner("Fetching instance info...", async () => { + const response = await bapiRequest({ + method: "GET", + path: "/v1/instance", + secretKey: keyless.secretKey, + }); + return response.body as { id?: string; environment_type?: string }; + }); + + return { + kind: "keyless", + instanceId: instance.id ?? null, + environmentType: instance.environment_type ?? null, + publishableKey: (await findLocalPublishableKey(process.cwd())) ?? null, + keySource: keyless.source, + }; +} + +function toJson(identity: Identity): Record { + if (identity.kind === "keyless") { + const { kind: _kind, ...keyless } = identity; + return { email: null, keyless, linked: null }; + } + + const resolved = identity.profile; + return { + email: identity.email, + linked: resolved + ? { + appId: resolved.profile.appId, + appName: resolved.profile.appName ?? null, + instances: { + development: resolved.profile.instances.development, + production: resolved.profile.instances.production ?? null, + }, + resolvedVia: resolved.resolvedVia, + path: resolved.path, + } + : null, + }; +} + +function render(identity: Identity): void { + if (identity.kind === "keyless") { + log.data(identity.instanceId ?? "unknown instance"); + log.info( + `Not logged in — running on an unclaimed keyless application (key from \`${identity.keySource}\`)`, ); + printNextSteps(NEXT_STEPS.WHOAMI); return; } - log.data(userInfo.email); - if (resolved) { - log.info(`Linked to \`${profileLabel(resolved.profile)}\``); + log.data(identity.email); + if (identity.profile) { + log.info(`Linked to \`${profileLabel(identity.profile.profile)}\``); } - printNextSteps(resolved ? NEXT_STEPS.WHOAMI_LINKED : NEXT_STEPS.WHOAMI); + printNextSteps(identity.profile ? NEXT_STEPS.WHOAMI_LINKED : NEXT_STEPS.WHOAMI); } export function registerWhoami(program: Program): void { diff --git a/packages/cli-core/src/lib/credential-store.ts b/packages/cli-core/src/lib/credential-store.ts index 5094b78d9..d2df0b207 100644 --- a/packages/cli-core/src/lib/credential-store.ts +++ b/packages/cli-core/src/lib/credential-store.ts @@ -436,6 +436,17 @@ export async function hasStoredCredentials(): Promise { return (await readStoredValue()) !== null; } +/** + * True when the CLI has credentials for a Clerk *account* — either a stored + * OAuth session or a platform API key. This is a presence check only: it does + * not hit the network, so an expired token or an API outage won't demote a + * logged-in user into an unauthenticated code path. + */ +export async function hasAccountCredentials(): Promise { + if (process.env.CLERK_PLATFORM_API_KEY) return true; + return hasStoredCredentials(); +} + export async function getValidToken(): Promise { const session = await getStoredSession(); if (!session) { diff --git a/packages/cli-core/src/lib/keyless-target.ts b/packages/cli-core/src/lib/keyless-target.ts new file mode 100644 index 000000000..fb6be9f42 --- /dev/null +++ b/packages/cli-core/src/lib/keyless-target.ts @@ -0,0 +1,196 @@ +/** + * Finding and addressing an unclaimed keyless application from the files a + * project already has on disk. + * + * Commands that can operate on an instance directly (config, feature toggles, + * whoami, env) resolve a target through here so the account and keyless paths + * are decided once, the same way, everywhere. + */ + +import { join } from "node:path"; +import { resolveAppContext, resolveProfile } from "./config.ts"; +import { hasAccountCredentials } from "./credential-store.ts"; +import { parseEnvFile } from "./dotenv.ts"; +import { CliError, ERROR_CODE, throwUsageError } from "./errors.ts"; +import { detectPublishableKeyName, detectSecretKeyName } from "./framework.ts"; +import { log } from "./log.ts"; + +export interface KeylessTarget { + secretKey: string; + /** Where the key came from, for display (`CLERK_SECRET_KEY env var`, `.env.local`). */ + source: string; +} + +export interface AccountContext { + appId: string; + appLabel: string; + instanceId: string; + instanceLabel: string; +} + +/** + * Where a command should send its reads and writes. Resolve this once and + * branch on `kind`, so the account and keyless paths can't drift per command. + */ +export type InstanceTarget = + | { kind: "account"; ctx: AccountContext; label: string } + | { kind: "keyless"; keyless: KeylessTarget; label: string }; + +const ENV_FILES = [".env", ".env.local"]; + +/** + * Where the Clerk SDKs park the keys for a keyless app they created themselves + * (running `next dev` with no keys configured). Shape: + * `{ publishableKey, secretKey, claimUrl, apiKeysUrl }`. + */ +const SDK_KEYLESS_FILE = [".clerk", ".tmp", "keyless.json"]; + +/** Reads the SDK's own keyless file, ignoring a partially-written one. */ +async function readSdkKeylessApp( + cwd: string, +): Promise<{ secretKey?: string; publishableKey?: string } | undefined> { + const file = Bun.file(join(cwd, ...SDK_KEYLESS_FILE)); + if (!(await file.exists())) return undefined; + + try { + const parsed = (await file.json()) as { secretKey?: unknown; publishableKey?: unknown }; + return { + secretKey: typeof parsed.secretKey === "string" ? parsed.secretKey : undefined, + publishableKey: typeof parsed.publishableKey === "string" ? parsed.publishableKey : undefined, + }; + } catch { + // A half-written file during an SDK refresh isn't worth failing over. + return undefined; + } +} + +interface LocatedKey { + value: string; + source: string; +} + +/** + * Looks for a key under any of `names`, in the order the app itself would + * resolve one: the environment first, then env files with a later file + * overriding an earlier one. + */ +async function findKeyInProject(cwd: string, names: string[]): Promise { + for (const name of new Set(names)) { + const value = process.env[name]; + if (value) return { value, source: `${name} env var` }; + } + + let found: LocatedKey | undefined; + for (const envFile of ENV_FILES) { + const file = Bun.file(join(cwd, envFile)); + if (!(await file.exists())) continue; + + for (const line of parseEnvFile(await file.text())) { + if (line.type !== "entry" || !line.value) continue; + if (names.includes(line.key)) found = { value: line.value, source: envFile }; + } + } + + return found; +} + +/** + * The instance secret key a keyless project keeps locally. Falls back to the + * keys an SDK created for itself, which it only does when nothing else supplies + * them — so that file goes last. + */ +export async function findLocalSecretKey(cwd: string): Promise { + const names = [await detectSecretKeyName(cwd), "CLERK_SECRET_KEY"]; + const located = await findKeyInProject(cwd, names); + + const found = located + ? { secretKey: located.value, source: located.source } + : await sdkKeylessTarget(cwd); + + if (found) log.debug(`keyless: secret key from ${found.source}`); + return found; +} + +async function sdkKeylessTarget(cwd: string): Promise { + const sdkApp = await readSdkKeylessApp(cwd); + if (!sdkApp?.secretKey) return undefined; + return { secretKey: sdkApp.secretKey, source: SDK_KEYLESS_FILE.join("/") }; +} + +/** The publishable key a keyless project holds locally, when one can be found. */ +export async function findLocalPublishableKey(cwd: string): Promise { + const names = [await detectPublishableKeyName(cwd), "CLERK_PUBLISHABLE_KEY"]; + const located = await findKeyInProject(cwd, names); + + return located?.value ?? (await readSdkKeylessApp(cwd))?.publishableKey; +} + +/** + * Resolves the keyless target for a command, or `undefined` when the + * account-authenticated path applies. + * + * Account credentials are deliberately NOT part of this decision: these + * commands must work from the instance secret key alone, with or without a + * platform API key or a login session. What rules keyless out is an explicit + * destination — `--app` or a linked profile — because that names an application + * the secret key on disk may not even belong to. + */ +export async function resolveKeylessTarget(options: { + app?: string; + instance?: string; + cwd?: string; +}): Promise { + if (options.app) return undefined; + + const cwd = options.cwd ?? process.cwd(); + if (await resolveProfile(cwd)) return undefined; + + const target = await findLocalSecretKey(cwd); + if (!target) return undefined; + + if (!target.secretKey.startsWith("sk_")) { + throw new CliError( + `Expected a secret key starting with \`sk_\` in ${target.source}, found something else.`, + { code: ERROR_CODE.INVALID_KEY_FORMAT }, + ); + } + + // The secret key addresses exactly one instance — its own — so there is no + // instance to choose between. + if (options.instance) { + throwUsageError( + `--instance is not supported for an unclaimed keyless application: the secret key in ${target.source} already targets its own instance.\n` + + "Run `clerk auth login` to claim the application, then target instances by name.", + ); + } + + // Signed in but unlinked is the one case where this fallback is surprising: + // the account could reach the full configuration if the project were linked. + // Say so rather than quietly answering with the smaller view. + if (await hasAccountCredentials()) { + log.warn( + `This directory isn't linked to an application — using the secret key from ${target.source}, which covers fewer settings.\n` + + "Run `clerk link` (or pass --app ) to use the full configuration.", + ); + } + + return target; +} + +export async function resolveInstanceTarget(options: { + app?: string; + instance?: string; + cwd?: string; +}): Promise { + const keyless = await resolveKeylessTarget(options); + if (keyless) { + return { + kind: "keyless", + keyless, + label: `this keyless application (secret key from ${keyless.source})`, + }; + } + + const ctx = await resolveAppContext(options); + return { kind: "account", ctx, label: `${ctx.appLabel} (${ctx.instanceLabel})` }; +} diff --git a/packages/cli-core/src/lib/keyless.ts b/packages/cli-core/src/lib/keyless.ts index b7fff3909..d87ea0b37 100644 --- a/packages/cli-core/src/lib/keyless.ts +++ b/packages/cli-core/src/lib/keyless.ts @@ -31,8 +31,20 @@ function isKeylessBreadcrumb(value: unknown): value is KeylessBreadcrumb { ); } +/** + * Application shapes the accountless endpoint can pre-configure at creation + * time — auth strategies, organizations, and billing are set server-side before + * the first key is ever used. + */ +export const KEYLESS_TEMPLATES = ["b2b-saas", "b2c-saas", "native", "waitlist"] as const; + +export type KeylessTemplate = (typeof KEYLESS_TEMPLATES)[number]; + /** Creates an accountless Clerk application via the public BAPI endpoint. */ -export async function createAccountlessApp(framework?: string): Promise { +export async function createAccountlessApp( + framework?: string, + template?: KeylessTemplate, +): Promise { const url = new URL("/v1/accountless_applications", getBapiBaseUrl()); const headers: Record = { @@ -41,7 +53,7 @@ export async function createAccountlessApp(framework?: string): Promise controller.abort(), CREATE_TIMEOUT_MS); diff --git a/packages/cli-core/src/test/integration/lib/harness.ts b/packages/cli-core/src/test/integration/lib/harness.ts index 5c08e7599..6d80b75cf 100644 --- a/packages/cli-core/src/test/integration/lib/harness.ts +++ b/packages/cli-core/src/test/integration/lib/harness.ts @@ -56,6 +56,8 @@ mock.module( } : null, hasStoredCredentials: async () => mockState.storedToken !== null, + hasAccountCredentials: async () => + Boolean(process.env.CLERK_PLATFORM_API_KEY) || mockState.storedToken !== null, storeToken: async (value: { accessToken: string }) => { mockState.storedToken = value.accessToken; }, diff --git a/packages/cli-core/src/test/lib/init-harness.ts b/packages/cli-core/src/test/lib/init-harness.ts new file mode 100644 index 000000000..f47e03c6c --- /dev/null +++ b/packages/cli-core/src/test/lib/init-harness.ts @@ -0,0 +1,186 @@ +/** + * Shared harness for the `clerk init` test files. + * + * `init` orchestrates a dozen collaborators, so every test needs the same wall + * of spies. Keeping that wall here lets the test files split by concern + * (strategy selection vs. bootstrap plumbing) without duplicating setup. + * + * Pure `spyOn` — no `mock.module`, which is process-lifetime in Bun and would + * leak into other files. + */ + +import { afterEach, spyOn } from "bun:test"; +import { useCaptureLog } from "./stubs.ts"; + +export * as loginMod from "../../commands/auth/login.ts"; +export * as linkMod from "../../commands/link/index.ts"; +export * as pullMod from "../../commands/env/pull.ts"; +export * as mode from "../../mode.ts"; +export * as config from "../../lib/config.ts"; +export * as frameworkMod from "../../lib/framework.ts"; +export * as context from "../../commands/init/context.ts"; +export * as scaffoldMod from "../../commands/init/scaffold.ts"; +export * as previewMod from "../../commands/init/preview.ts"; +export * as formatMod from "../../commands/init/format.ts"; +export * as scanMod from "../../commands/init/scan.ts"; +export * as heuristics from "../../commands/init/heuristics.ts"; +export * as skillsMod from "../../commands/init/skills.ts"; +export * as bootstrapMod from "../../commands/init/bootstrap.ts"; +export * as nextStepsMod from "../../lib/next-steps.ts"; +export * as keylessMod from "../../lib/keyless.ts"; + +import * as loginModule from "../../commands/auth/login.ts"; +import * as linkModule from "../../commands/link/index.ts"; +import * as pullModule from "../../commands/env/pull.ts"; +import * as modeModule from "../../mode.ts"; +import * as configModule from "../../lib/config.ts"; +import * as frameworkModule from "../../lib/framework.ts"; +import * as contextModule from "../../commands/init/context.ts"; +import * as scaffoldModule from "../../commands/init/scaffold.ts"; +import * as previewModule from "../../commands/init/preview.ts"; +import * as formatModule from "../../commands/init/format.ts"; +import * as scanModule from "../../commands/init/scan.ts"; +import * as heuristicsModule from "../../commands/init/heuristics.ts"; +import * as skillsModule from "../../commands/init/skills.ts"; +import * as bootstrapModule from "../../commands/init/bootstrap.ts"; +import * as keylessModule from "../../lib/keyless.ts"; + +export const FAKE_CTX = { + cwd: "/tmp/test", + framework: { + dep: "react", + name: "React", + sdk: "@clerk/react", + envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env" as const, + }, + typescript: true, + srcDir: false, + packageManager: "npm" as const, + existingClerk: true, + deps: { react: "^19.0.0" }, + envFile: ".env", +}; + +export const FAKE_BOOTSTRAP = { + projectDir: "/tmp/test/my-app", + projectName: "my-app", + packageManager: "npm" as const, +}; + +type FakeFramework = { + dep: string; + name: string; + sdk: string; + envVar: string; + envFile: ".env" | ".env.local"; + supportsKeyless?: boolean; +}; + +export type FakeCtx = Omit & { framework: FakeFramework }; + +export const KEYLESS_CTX: FakeCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { ...FAKE_CTX.framework, supportsKeyless: true }, +}; + +export function mockBootstrapTo(ctx: FakeCtx): void { + spyOn(contextModule, "gatherContext").mockResolvedValueOnce(null).mockResolvedValueOnce(ctx); +} + +export function mockExistingProject(ctx: FakeCtx): void { + spyOn(contextModule, "gatherContext").mockResolvedValue(ctx); +} + +export function mockMiddlewareScaffold(): void { + spyOn(scaffoldModule, "scaffold").mockResolvedValue({ + actions: [{ type: "create", path: "middleware.ts", content: "", description: "" }], + postInstructions: [], + }); +} + +export interface InitHarness { + setup: (overrides?: { email?: string | null; apiKey?: boolean; isAgent?: boolean }) => { + gatherContextSpy: ReturnType; + captured: ReturnType; + }; + setupBootstrapSuccess: () => void; + /** Registers an extra spy so the harness restores it with the rest. */ + track: (spy: ReturnType) => void; + captured: ReturnType; +} + +/** + * Registers the spy lifecycle for an `init` describe block. Call at describe + * scope, exactly like `useCaptureLog()`. + */ +export function useInitHarness(): InitHarness { + let spies: ReturnType[] = []; + const captured = useCaptureLog(); + + afterEach(() => { + for (const s of spies) s.mockRestore(); + spies = []; + }); + + function setup(overrides: { email?: string | null; apiKey?: boolean; isAgent?: boolean } = {}) { + const email = overrides.email ?? null; + const apiKey = overrides.apiKey ?? false; + const agent = overrides.isAgent ?? false; + const authed = email != null || apiKey; + const gatherContextSpy = spyOn(contextModule, "gatherContext").mockResolvedValue(null); + + spies = [ + spyOn(modeModule, "isAgent").mockReturnValue(agent), + spyOn(modeModule, "isHuman").mockReturnValue(!agent), + spyOn(configModule, "resolveProfile").mockResolvedValue(undefined), + spyOn(frameworkModule, "lookupFramework").mockReturnValue(null), + gatherContextSpy, + spyOn(contextModule, "hasPackageJson").mockResolvedValue(false), + spyOn(scaffoldModule, "scaffold").mockResolvedValue({ actions: [], postInstructions: [] }), + spyOn(scaffoldModule, "enrichProjectContext").mockResolvedValue(undefined), + spyOn(previewModule, "previewPlan").mockReturnValue(undefined), + spyOn(previewModule, "previewAndConfirm").mockResolvedValue(true), + spyOn(formatModule, "runFormatters").mockResolvedValue(undefined), + spyOn(scanModule, "detectAuthLibraries").mockReturnValue(undefined), + spyOn(scanModule, "scanForIssues").mockResolvedValue([]), + spyOn(heuristicsModule, "getAuthenticatedEmail").mockResolvedValue(email), + spyOn(heuristicsModule, "isAuthenticated").mockResolvedValue(authed), + spyOn(heuristicsModule, "printKeylessInfo").mockReturnValue(undefined), + spyOn(heuristicsModule, "installSdk").mockResolvedValue(undefined), + spyOn(heuristicsModule, "installDeps").mockResolvedValue(undefined), + spyOn(heuristicsModule, "writePlan").mockResolvedValue([]), + spyOn(heuristicsModule, "checkGitDirty").mockResolvedValue(false), + spyOn(heuristicsModule, "printOutro").mockReturnValue(undefined), + spyOn(skillsModule, "installSkills").mockResolvedValue(undefined), + spyOn(loginModule, "login").mockResolvedValue(undefined as never), + spyOn(linkModule, "link").mockResolvedValue(undefined), + spyOn(pullModule, "pull").mockResolvedValue(undefined), + spyOn(bootstrapModule, "promptAndBootstrap").mockResolvedValue(FAKE_BOOTSTRAP), + spyOn(bootstrapModule, "confirmOverwrite").mockResolvedValue(undefined), + spyOn(keylessModule, "createAccountlessApp").mockResolvedValue({ + publishable_key: "pk_test_stub", + secret_key: "sk_test_stub", + claim_url: "/apps/claim?token=stub_token", + }), + spyOn(keylessModule, "writeKeysToEnvFile").mockResolvedValue(undefined), + spyOn(keylessModule, "writeKeylessBreadcrumb").mockResolvedValue(undefined), + ]; + + return { gatherContextSpy, captured }; + } + + function setupBootstrapSuccess(): void { + const gatherSpy = + spies.find((s) => s.getMockName?.() === "gatherContext") ?? + spyOn(contextModule, "gatherContext"); + gatherSpy.mockResolvedValueOnce(null).mockResolvedValueOnce(FAKE_CTX); + } + + function track(spy: ReturnType): void { + spies.push(spy); + } + + return { setup, setupBootstrapSuccess, track, captured }; +} diff --git a/packages/cli-core/src/test/lib/stubs.ts b/packages/cli-core/src/test/lib/stubs.ts index 1fbf961dd..13e6f8db1 100644 --- a/packages/cli-core/src/test/lib/stubs.ts +++ b/packages/cli-core/src/test/lib/stubs.ts @@ -126,21 +126,35 @@ export function captureUi() { const noop = async () => {}; +// Mocking a module replaces it wholesale, so this must cover every export of +// lib/config.ts — a missing name is an import error in any consumer, not just +// the one under test. export const configStubs = { _setConfigDir: () => {}, + getConfigFile: () => "", readConfig: noop, writeConfig: noop, getAuth: noop, setAuth: noop, clearAuth: noop, + getEnvironment: noop, + setEnvironment: noop, getProfile: noop, setProfile: noop, removeProfile: noop, moveProfile: noop, listProfiles: noop, + getRelayEntry: noop, + setRelayEntry: noop, resolveProfile: noop, resolveProfileOrAutolink: noop, resolveInstanceId: () => ({ id: "", label: "" }), + resolveFetchedApplicationInstance: () => ({ + found: false, + instanceId: "", + instanceLabel: "", + instance: undefined, + }), resolveAppContext: async () => ({ appId: "", appLabel: "", instanceId: "", instanceLabel: "" }), profileLabel: (profile: { appName?: string; appId: string }) => profile.appName ? `${profile.appName} (${profile.appId})` : profile.appId, @@ -158,6 +172,7 @@ export const credentialStoreStubs = { getValidToken: async () => null, getStoredSession: async () => null, hasStoredCredentials: async () => false, + hasAccountCredentials: async () => Boolean(process.env.CLERK_PLATFORM_API_KEY), storeToken: async () => {}, deleteToken: async () => {}, createOAuthSession: (tokenResponse: { From 8632c3ac6dd077bd3f5e5d48e94518d90717ed12 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Tue, 28 Jul 2026 13:12:35 -0300 Subject: [PATCH 02/10] feat(keyless): configure an unclaimed application end to end without an account MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Close the gaps between "keyless is the default" and "keyless actually works with no human intervention": - config patch validates instance fields against the Backend API's own schema before sending — PATCH /v1/instance answers 204 and silently discards unknown fields, so a typo or an account-only setting (password, social, second_factors, ...) used to print "Config pushed successfully" and change nothing. The refusal names the accepted fields and, for settings BAPI has no route for at all, says claiming is the only path. - config patch no longer prints a stale eventually-consistent re-read as the result of a write; bodyless groups are reported as unconfirmed. - allowlist_identifiers/blocklist_identifiers point at `clerk api` (which reaches them keyless) instead of demanding a login. - users open explains that an unclaimed app has no dashboard page and offers `clerk api /users/`, instead of blaming "not linked" and suggesting remedies that need an application ID that doesn't exist yet. - whoami/env pull detect publishable/secret keys that belong to different applications; env pull refuses to write a mismatched pair. - doctor, open, users, api, env pull, whoami, enable/disable orgs and templates verified end to end on a live unclaimed instance, all under --mode agent with non-TTY stdin: no hangs, no prompts, no browser. docs/keyless-demos: before/after screenshots proving CLI writes reach the live hosted UI pre-claim (orgs step inserted into the sign-in flow, allowlist enforced then lifted), with a README on the FAPI /v1/environment oracle and its limits. --- .changeset/keyless-mode-default.md | 15 +- docs/keyless-demos/.gitignore | 2 + docs/keyless-demos/README.md | 80 ++++++ docs/keyless-demos/lib/demo-env.sh | 107 +++++++ docs/keyless-demos/record.sh | 59 ++++ .../screenshots/01-signin-before.png | Bin 0 -> 25144 bytes .../screenshots/02-signup-before.png | Bin 0 -> 29797 bytes .../screenshots/03-orgs-enabled-after.png | Bin 0 -> 30771 bytes .../screenshots/04-orgs-disabled-after.png | Bin 0 -> 133851 bytes .../05-signup-restricted-after.png | Bin 0 -> 40250 bytes .../screenshots/06-signup-allowed-after.png | Bin 0 -> 19898 bytes docs/keyless-demos/screenshots/README.md | 56 ++++ docs/keyless-demos/tapes/01-bootstrap.tape | 22 ++ docs/keyless-demos/tapes/02-read-config.tape | 30 ++ .../keyless-demos/tapes/03-change-config.tape | 39 +++ docs/keyless-demos/tapes/04-toggles.tape | 29 ++ docs/keyless-demos/tapes/05-templates.tape | 25 ++ docs/keyless-demos/tapes/06-guardrails.tape | 33 +++ docs/keyless-demos/tapes/07-agent-mode.tape | 35 +++ .../tapes/08-sso-connections.tape | 37 +++ .../tapes/09-health-and-users.tape | 35 +++ docs/keyless-demos/tapes/_common.tape | 19 ++ packages/cli-core/src/commands/api/README.md | 22 +- .../cli-core/src/commands/config/README.md | 50 +++- .../src/commands/config/apply-patch.ts | 10 +- packages/cli-core/src/commands/config/io.ts | 31 +- .../src/commands/config/keyless.test.ts | 267 ++++++++++++++++-- .../cli-core/src/commands/config/keyless.ts | 230 +++++++++++++-- packages/cli-core/src/commands/config/push.ts | 48 +++- .../cli-core/src/commands/doctor/README.md | 33 ++- .../cli-core/src/commands/doctor/checks.ts | 141 +++++++-- .../src/commands/doctor/context.test.ts | 87 ++++++ .../cli-core/src/commands/doctor/context.ts | 42 ++- .../src/commands/doctor/doctor.test.ts | 123 +++++++- .../cli-core/src/commands/doctor/types.ts | 15 + packages/cli-core/src/commands/env/README.md | 13 +- .../cli-core/src/commands/env/pull.test.ts | 60 ++++ packages/cli-core/src/commands/env/pull.ts | 19 ++ packages/cli-core/src/commands/init/README.md | 49 ++-- .../cli-core/src/commands/init/heuristics.ts | 12 + packages/cli-core/src/commands/init/index.ts | 140 ++++++++- .../src/commands/init/strategy.test.ts | 244 +++++++++++++++- packages/cli-core/src/commands/open/README.md | 62 ++++ .../cli-core/src/commands/open/index.test.ts | 140 ++++++++- packages/cli-core/src/commands/open/index.ts | 100 ++++++- .../src/commands/open/keyless-claim.test.ts | 145 ++++++++++ .../src/commands/open/keyless-claim.ts | 99 +++++++ .../cli-core/src/commands/users/README.md | 21 +- .../cli-core/src/commands/users/open.test.ts | 48 +++- packages/cli-core/src/commands/users/open.ts | 33 +++ .../cli-core/src/commands/whoami/README.md | 12 +- .../src/commands/whoami/index.test.ts | 149 ++++++++++ .../cli-core/src/commands/whoami/index.ts | 69 ++++- .../cli-core/src/lib/bapi-command.test.ts | 62 +++- packages/cli-core/src/lib/bapi-command.ts | 37 ++- packages/cli-core/src/lib/credential-store.ts | 25 ++ .../cli-core/src/lib/keyless-target.test.ts | 181 ++++++++++++ packages/cli-core/src/lib/keyless-target.ts | 115 +++++++- 58 files changed, 3372 insertions(+), 185 deletions(-) create mode 100644 docs/keyless-demos/.gitignore create mode 100644 docs/keyless-demos/README.md create mode 100644 docs/keyless-demos/lib/demo-env.sh create mode 100755 docs/keyless-demos/record.sh create mode 100644 docs/keyless-demos/screenshots/01-signin-before.png create mode 100644 docs/keyless-demos/screenshots/02-signup-before.png create mode 100644 docs/keyless-demos/screenshots/03-orgs-enabled-after.png create mode 100644 docs/keyless-demos/screenshots/04-orgs-disabled-after.png create mode 100644 docs/keyless-demos/screenshots/05-signup-restricted-after.png create mode 100644 docs/keyless-demos/screenshots/06-signup-allowed-after.png create mode 100644 docs/keyless-demos/screenshots/README.md create mode 100644 docs/keyless-demos/tapes/01-bootstrap.tape create mode 100644 docs/keyless-demos/tapes/02-read-config.tape create mode 100644 docs/keyless-demos/tapes/03-change-config.tape create mode 100644 docs/keyless-demos/tapes/04-toggles.tape create mode 100644 docs/keyless-demos/tapes/05-templates.tape create mode 100644 docs/keyless-demos/tapes/06-guardrails.tape create mode 100644 docs/keyless-demos/tapes/07-agent-mode.tape create mode 100644 docs/keyless-demos/tapes/08-sso-connections.tape create mode 100644 docs/keyless-demos/tapes/09-health-and-users.tape create mode 100644 docs/keyless-demos/tapes/_common.tape create mode 100644 packages/cli-core/src/commands/open/README.md create mode 100644 packages/cli-core/src/commands/open/keyless-claim.test.ts create mode 100644 packages/cli-core/src/commands/open/keyless-claim.ts create mode 100644 packages/cli-core/src/lib/keyless-target.test.ts diff --git a/.changeset/keyless-mode-default.md b/.changeset/keyless-mode-default.md index eea8488a9..3c3f0b423 100644 --- a/.changeset/keyless-mode-default.md +++ b/.changeset/keyless-mode-default.md @@ -8,12 +8,19 @@ Make keyless mode the default for unauthenticated `clerk init` runs on keyless-c - Agent-mode runs while signed out now set up keyless keys with a claim breadcrumb instead of printing manual setup guidance. - `--keyless` now forces keyless mode even when signed in, including in existing projects. Combining it with `--login` or `--app` exits with a usage error. - `--login` in agent mode while signed out exits with a usage error, since the browser login needs an interactive terminal. +- Re-running init on a project that already holds an unclaimed keyless application now keeps that application instead of silently minting a second one and orphaning the first. `--fresh` asks for a replacement explicitly; neither `-y` nor agent mode is treated as consent to discard one. +- Agent mode no longer treats the mere presence of stored credentials as being signed in. A session that turns out expired used to send an agent into a browser login it could never complete; init now validates the session and falls back to keyless. +- `--template` and `--fresh` exit with a usage error naming the reason when the run doesn't resolve to keyless, instead of being silently dropped. Add keyless support to `clerk config pull` and `clerk config patch`, so an unclaimed keyless application can be configured without logging in. -- When the project isn't linked and no `--app` is passed, both commands use the instance secret key the project already holds and talk to the Backend API instead of the Platform API. This needs no account at all — no login and no `CLERK_PLATFORM_API_KEY`. If credentials happen to be present, the command still works and warns that linking would give the full configuration. +- When the project isn't linked and no `--app` is passed, both commands use the instance secret key the project already holds and talk to the Backend API instead of the Platform API. This needs no account at all — no login and no `CLERK_PLATFORM_API_KEY`. If credentials happen to be present, the command still works and warns that linking would give the full configuration; when the stored session has expired, that warning says so rather than pointing at a `clerk link` that can't succeed yet. +- `clerk config patch` no longer reports a field as pushed when the Backend API dropped it. The API ignores field names it doesn't recognise inside a group rather than rejecting them, so a typo used to end in "Config pushed successfully" with nothing applied; the write is now checked against the API's own response and unrecognised fields are named. Groups whose write answers with no body are reported as unconfirmed rather than guessed at. - Keyless payloads name Backend API resources directly: `instance`, `communication`, `restrictions`, `organization_settings`, `protect`, `oauth_application_settings`, and `instance_settings` (`test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist`). Any other top-level key exits with a usage error naming the supported ones. - `clerk config pull` returns the same envelope; `restrictions` and `instance_settings` are omitted because the Backend API has no read route for them. +- `clerk config patch` now rejects unknown fields on the `instance` group before sending anything. That route answers `204` with no body and silently discards field names it doesn't recognise, so `{"instance": {"password": "on"}}` used to print "Config pushed successfully" and change nothing at all. The error names the fields the route does accept, and for the auth settings people reach for here — password, phone number, username, passkeys, second factors, social providers — says that Clerk's Backend API has no route for them at all, so no retyping will help. +- `clerk config patch` no longer prints a stale re-read as the result of a write. The `instance` group answers with no body, and the follow-up read that used to fill the gap is eventually consistent — it routinely returned the pre-write value, which then appeared directly beneath "Config pushed successfully" as though the write had been ignored. The group now contributes no state, and the success line names it as unconfirmed. +- `allowlist_identifiers` and `blocklist_identifiers` in a config payload now point at `clerk api`, which reaches them on an unclaimed application today, instead of telling the user to `clerk auth login` and claim the app first. - In keyless mode `--dry-run` previews the diff locally without sending anything, `--instance` exits with a usage error (the secret key already targets one instance), and `clerk config put` and `clerk config schema` explain that they need a claimed application. Let more commands operate on an unclaimed keyless application, so an agent can bootstrap and configure Clerk end to end without an account. @@ -23,3 +30,9 @@ Let more commands operate on an unclaimed keyless application, so an agent can b - `clerk env pull` writes a keyless application's locally-held keys into the project's env file instead of failing with "not linked". - Every keyless-capable command now also finds the keys a Clerk SDK created for itself in `.clerk/.tmp/keyless.json`, so an application minted by running the dev server is reachable from the CLI. - `clerk init --template ` pre-configures the keyless application at creation — a `b2b-saas` app comes back with organizations already enabled. Keyless-only; combining it with `--login` exits with a usage error. +- `clerk users`, `clerk api`, and the rest of the Backend API command surface now resolve a keyless project's own secret key from its env files. They appeared to work in development only because Bun loads `.env.local` automatically; the released binary does not, so on a real machine every one of them failed with "no secret key found" in a directory where `clerk whoami` happily named the instance. +- `clerk doctor` understands keyless projects. An unclaimed keyless application is reported as a healthy state naming the instance, instead of two failures pushing `clerk auth login` and `clerk link`; checks that genuinely need an account are skipped rather than failed. The report also says how this particular application can be claimed, since `clerk auth login` only claims silently when `clerk init` left a claim token behind. +- `clerk open` finds a keyless application's claim link instead of dead-ending on "Run `clerk link` first", which cannot claim an unclaimed application. +- `clerk whoami` and `clerk env pull` now check that a project's publishable and secret keys belong to the same application. They are found independently, so a leftover key from another project used to be reported — and written — as a matched pair, leaving the server trusting one application while the browser talked to another. `whoami` warns and still reports; `env pull` refuses to write. +- Commands no longer leak `Token refresh failed (401)` from the credential store when a stored session can't be refreshed. Any refusal from the token endpoint now reads as an expired session, and `clerk whoami` falls through to the keyless application rather than aborting. +- `clerk users open ` explains that an unclaimed application has no dashboard page — a dashboard link needs an application ID, and one is only assigned on claiming — instead of reporting that nothing is linked and pointing at `clerk link` and `--app`, neither of which can name an application that does not exist yet. The error suggests claiming the app, and `clerk api /users/` to read the user without claiming. diff --git a/docs/keyless-demos/.gitignore b/docs/keyless-demos/.gitignore new file mode 100644 index 000000000..8dc6a2d3f --- /dev/null +++ b/docs/keyless-demos/.gitignore @@ -0,0 +1,2 @@ +bin/ +out/ diff --git a/docs/keyless-demos/README.md b/docs/keyless-demos/README.md new file mode 100644 index 000000000..a9de1502f --- /dev/null +++ b/docs/keyless-demos/README.md @@ -0,0 +1,80 @@ +# Keyless demos + +Recorded walkthroughs of what the CLI can do against an **unclaimed keyless +application** — a Clerk app created anonymously, with no account, no login, and +no browser round-trip. + +Each tape is a script of exactly what is shown. Re-record them with: + +```sh +./docs/keyless-demos/record.sh # every tape +./docs/keyless-demos/record.sh 03 06 # just these +``` + +Requires [VHS](https://github.com/charmbracelet/vhs) (`brew install vhs`). + +## How they run + +`clerk` on the tapes' `PATH` is a shim that runs this working tree, so a +recording always shows _this checkout_ rather than a globally installed CLI. + +It runs the TypeScript entry point rather than the `build:compile` binary on +purpose: a locally built binary is unsigned, so macOS raises a keychain +authorization dialog the first time it reads the credential store — which a +headless recording can never answer, and the tape hangs. The shim passes +`--env-file=/dev/null` so Bun's `.env` autoload stays out of the way and the CLI +resolves keys from the project's files itself, exactly as the signed release +binary does. Without it, every key would be reported as coming from an +environment variable rather than from `.env.local`. + +Every tape mints its own keyless application against the real Clerk API through +the public `POST /v1/accountless_applications` endpoint — the same one +`clerk init --keyless` uses. Nothing needs cleaning up afterwards: unclaimed +applications are throwaway by construction. + +### The `[KEYLESS-DEMO]` banner + +These demos are about a developer who has never logged in, recorded on a machine +that has. `signed_out()` in `lib/demo-env.sh` points the CLI at an empty config +directory and at an environment named `keyless-demo`. Sessions are keyed +`oauth-access-token:` in the OS keyring, so that name alone misses +every stored credential without reading, touching, or deleting any of them — and +the CLI prints its non-production banner as a result. + +It is only the credential store that is isolated. Both environment profiles hold +the real Clerk URLs: every request in every recording goes to `api.clerk.com` +against a genuine application. + +Only publishable keys and instance IDs ever reach the screen. Secret keys go +straight into `.env.local`, which no tape prints. If you add a tape, keep it +that way. + +## The tapes + +| Tape | What it shows | +| -------------------------- | ------------------------------------------------------------------------------------------- | +| `01-bootstrap.tape` | `clerk init --keyless` on a signed-out machine, then `clerk whoami` naming the new instance | +| `02-read-config.tape` | `clerk config pull`, whole and by `--keys`, authenticated only by the local secret key | +| `03-change-config.tape` | `clerk config patch` — dry run, apply, read back, multi-group payload | +| `04-toggles.tape` | `clerk enable orgs` / `clerk disable orgs`, including the idempotent second run | +| `05-templates.tape` | `clerk init --template b2b-saas`, which returns an app with organizations already on | +| `06-guardrails.tape` | The refusals: `config put`, an account-only config key, `--instance`, `enable billing` | +| `07-agent-mode.tape` | The same application driven by `--mode agent`, where every answer is JSON | +| `08-sso-connections.tape` | Adding and removing an enterprise SSO connection, which needs no account | +| `09-health-and-users.tape` | `clerk doctor`, `clerk users list` and `clerk open --print` on a keyless project | + +## Layout + +``` +docs/keyless-demos/ +├── record.sh # write the `clerk` shim, then run the tapes +├── lib/demo-env.sh # sourced by each tape inside a Hide block +├── tapes/ # one tape per workflow; _common.tape holds the shared look +├── bin/ # the `clerk` shim under test (gitignored) +└── out/ # rendered gif + mp4 (gitignored) +``` + +The throwaway projects the tapes build go under `$TMPDIR/clerk-keyless-demos`, +outside the repository. Inside a dirty git worktree, `clerk init` opens by +telling you to commit first — correct advice, and pure noise in a recording +about something else. diff --git a/docs/keyless-demos/lib/demo-env.sh b/docs/keyless-demos/lib/demo-env.sh new file mode 100644 index 000000000..0812ee7d9 --- /dev/null +++ b/docs/keyless-demos/lib/demo-env.sh @@ -0,0 +1,107 @@ +# Helpers the demo tapes source inside a `Hide` block, so the recording shows +# the Clerk commands rather than the scaffolding around them. +# +# Sourced, never executed: `new_project` has to change the caller's directory. + +# A quiet prompt with no history file, so recordings are reproducible. +quiet_shell() { + export PS1='$ ' + export PROMPT_COMMAND= + export HISTFILE=/dev/null + # Keep the CLI's own update check out of the recording. + export CLERK_NO_UPDATE_CHECK=1 +} + +# These demos are about a developer who has never logged in, but they get +# recorded on a machine that has. Point the CLI at an empty config directory and +# at an environment name nothing is stored under, so it genuinely finds no +# account — rather than faking one with a mock API. +# +# Credentials are keyed `oauth-access-token:` in the OS keyring, so +# naming an environment other than `production` is enough to miss every stored +# session without touching, reading, or deleting any of them. Both profiles hold +# the real Clerk URLs: every request in these recordings goes to the live API. +signed_out() { + local dir="${1:?config dir required}" + mkdir -p "$dir" + printf '{"environment":"keyless-demo"}\n' > "$dir/config.json" + export CLERK_CONFIG_DIR="$dir" + + local profile='{ + "oauthClientId": "ins_1lyWDZiobr600AKUeQDoSlrEmoM", + "oauthBaseUrl": "https://clerk.clerk.com", + "platformApiUrl": "https://api.clerk.com", + "backendApiUrl": "https://api.clerk.dev", + "dashboardUrl": "https://dashboard.clerk.com" + }' + printf '{"production": %s, "keyless-demo": %s}\n' "$profile" "$profile" > .env-profiles.json +} + +# Fresh Next.js-shaped project at work/, and cd into it. The `next` +# dependency is what drives the CLI's framework detection and the +# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY naming. +# +# Built outside the repository on purpose. A project inside a dirty git worktree +# makes `clerk init` open with "You have uncommitted changes", which is correct +# advice and pure noise in a recording about something else. +new_project() { + local name="${1:?project name required}" + local root="${TMPDIR:-/tmp}/clerk-keyless-demos" + + rm -rf "$root/$name" + mkdir -p "$root/$name" + cd "$root/$name" || return 1 + + cat > package.json <<'JSON' +{ + "name": "acme-dashboard", + "private": true, + "dependencies": { + "next": "^15.0.0", + "react": "^19.0.0", + "@clerk/nextjs": "^6.0.0" + } +} +JSON + + signed_out "$root/.config-$name" +} + +# Mint an unclaimed keyless application straight from Clerk's public endpoint +# and drop its keys where the CLI will find them. Demos that are *about* +# configuring a keyless app start here instead of replaying `clerk init`. +# +# The endpoint rate-limits, and a half-written .env.local is worse than no +# recording at all — it makes the CLI fall through to the account path and the +# demo silently shows the wrong thing. So: retry, then fail loudly. +# +# $1 — optional application template (b2b-saas, b2c-saas, native, waitlist) +keyless_keys() { + local template="${1:-}" + local body="source=cli" + [ -n "$template" ] && body="$body&template=$template" + + local response publishable secret attempt + for attempt in 1 2 3 4 5; do + response="$(curl -sS -X POST https://api.clerk.com/v1/accountless_applications \ + -H 'Content-Type: application/x-www-form-urlencoded' \ + -d "$body")" + + publishable="$(printf '%s' "$response" | grep -o '"publishable_key":"[^"]*"' | cut -d'"' -f4)" + secret="$(printf '%s' "$response" | grep -o '"secret_key":"[^"]*"' | cut -d'"' -f4)" + + if [ -n "$publishable" ] && [ -n "$secret" ]; then + cat > .env.local <&2 + sleep $((attempt * 5)) + done + + echo "keyless_keys: could not mint a keyless application; refusing to record" >&2 + return 1 +} diff --git a/docs/keyless-demos/record.sh b/docs/keyless-demos/record.sh new file mode 100755 index 000000000..a7594696d --- /dev/null +++ b/docs/keyless-demos/record.sh @@ -0,0 +1,59 @@ +#!/usr/bin/env bash +# +# Re-record the keyless demos. +# +# ./docs/keyless-demos/record.sh # every tape +# ./docs/keyless-demos/record.sh 03 06 # only tapes whose name starts with 03 or 06 +# +# Each run compiles the CLI from the current working tree and puts that binary +# first on PATH, so what you see recorded is this checkout — not whatever +# `clerk` happens to be installed globally. +# +# Every tape mints its own unclaimed keyless application against the real Clerk +# API. No account, no login, nothing to clean up: unclaimed applications expire +# on their own. Only publishable keys and instance IDs ever reach the screen. + +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +repo="$(cd "$here/../.." && pwd)" + +command -v vhs >/dev/null || { echo "vhs is not installed: brew install vhs" >&2; exit 1; } + +mkdir -p "$here/bin" "$here/out" "$here/work" + +# `clerk` on the tapes' PATH is this working tree, so a recording always shows +# the current checkout rather than a globally installed CLI. +# +# It runs the TypeScript entry point rather than `build:compile`'s binary on +# purpose. A locally built binary is unsigned, so macOS puts up a keychain +# authorization dialog the first time it reads the credential store — which a +# headless recording can never answer, and the tape hangs. `--env-file=/dev/null` +# turns off Bun's .env autoload so the CLI resolves keys from the project's files +# itself, exactly as the signed release binary does (it is compiled with +# --no-compile-autoload-dotenv). Without it the CLI would report every key as +# coming from an environment variable. +cat > "$here/bin/clerk" <&2; exit 1; } + +for tape in "${tapes[@]}"; do + echo "==> recording $(basename "$tape")" + (cd "$here/tapes" && vhs "$(basename "$tape")") +done + +echo "==> done, output in $here/out" +ls -la "$here/out" diff --git a/docs/keyless-demos/screenshots/01-signin-before.png b/docs/keyless-demos/screenshots/01-signin-before.png new file mode 100644 index 0000000000000000000000000000000000000000..1df48965116d3c4746a6667505cb6484599411c6 GIT binary patch literal 25144 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 B4N?FA literal 0 HcmV?d00001 diff --git a/docs/keyless-demos/screenshots/02-signup-before.png b/docs/keyless-demos/screenshots/02-signup-before.png new file mode 100644 index 0000000000000000000000000000000000000000..054dbf60c7804ce5ef1864e81c71a3b63e81a39a GIT binary patch literal 29797 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 Cy)tM3 literal 0 HcmV?d00001 diff --git a/docs/keyless-demos/screenshots/03-orgs-enabled-after.png b/docs/keyless-demos/screenshots/03-orgs-enabled-after.png new file mode 100644 index 0000000000000000000000000000000000000000..3605f4865df6ef75bfbd4cb814b288bb04f751b5 GIT binary patch literal 30771 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 literal 0 HcmV?d00001 diff --git a/docs/keyless-demos/screenshots/04-orgs-disabled-after.png b/docs/keyless-demos/screenshots/04-orgs-disabled-after.png new file mode 100644 index 0000000000000000000000000000000000000000..93a907ff6f7fffd3f45f5e719652b8651cfd4d95 GIT binary patch literal 133851 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

_}1*_+@+pRJ_nqeZ?Z$Zf}lu21b~gkP6#S%!Bf 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

QOJ4o!V}w-vUWd4bR2N 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 kdl!zO@X>_>F(~5jzxFu$KQYN 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 Q5%?n~tt^!<@%Y961u!c1z5oCK literal 0 HcmV?d00001 diff --git a/docs/keyless-demos/screenshots/06-signup-allowed-after.png b/docs/keyless-demos/screenshots/06-signup-allowed-after.png new file mode 100644 index 0000000000000000000000000000000000000000..6e65199329a3c1da3c5a8c2e0f6479553549ce4d GIT binary patch literal 19898 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/v1/environment?__clerk_api_version=2025-04-10&_clerk_js_version=5.100.0" +``` + +`clerk enable orgs` moves `organization_settings.enabled` there from `false` to +`true`, which is why the sign-in step appears. + +Restrictions are different, and the difference is worth knowing before you go +looking for it in the wrong place. Enabling the allowlist leaves +`user_settings.sign_up.mode` reading `public` — that field describes what the +sign-up form should _render_, and the form still renders. The rule is enforced +when the sign-up is submitted. So the only honest proof for a restriction is to +attempt the operation and be refused, which is what `05` and `06` capture. + +## Re-creating these + +They were captured against a throwaway application whose keys are long gone; +the URLs in them will not resolve. To reproduce, mint your own and drive the +portal at the `accounts_portal_url` from: + +```sh +clerk api /domains # works with only an instance secret key +``` + +Nothing needs cleaning up. Unclaimed applications are throwaway by construction. diff --git a/docs/keyless-demos/tapes/01-bootstrap.tape b/docs/keyless-demos/tapes/01-bootstrap.tape new file mode 100644 index 000000000..191bdfd45 --- /dev/null +++ b/docs/keyless-demos/tapes/01-bootstrap.tape @@ -0,0 +1,22 @@ +# Bootstrap a Clerk application with no account, no browser, no login. + +Output ../out/01-bootstrap.gif +Output ../out/01-bootstrap.mp4 + +Source "_common.tape" + +Hide +Type "source ../lib/demo-env.sh && quiet_shell && new_project bootstrap && clear" Enter +Show + +Sleep 1s +Type "clerk init --framework next --keyless --yes --no-skills" +Sleep 700ms +Enter +Sleep 30s + +Type "clerk whoami" +Sleep 500ms +Enter +Sleep 8s +Sleep 2s diff --git a/docs/keyless-demos/tapes/02-read-config.tape b/docs/keyless-demos/tapes/02-read-config.tape new file mode 100644 index 000000000..6859c44fe --- /dev/null +++ b/docs/keyless-demos/tapes/02-read-config.tape @@ -0,0 +1,30 @@ +# Read the instance configuration of an unclaimed keyless application. +# The only credential in play is the secret key already sitting in .env.local. + +Output ../out/02-read-config.gif +Output ../out/02-read-config.mp4 + +Source "_common.tape" + +Hide +Type "source ../lib/demo-env.sh && quiet_shell && new_project read-config && keyless_keys && clear" Enter +Show + +Sleep 1s +Type "clerk config pull --keys protect" +Sleep 500ms +Enter +Sleep 8s + +Type "# every readable group at once" +Enter +Type "clerk config pull | jq 'keys'" +Sleep 500ms +Enter +Sleep 8s + +Type "clerk config pull --keys organization_settings | jq '.organization_settings | {enabled, max_allowed_memberships, domains_enabled}'" +Sleep 500ms +Enter +Sleep 9s +Sleep 2s diff --git a/docs/keyless-demos/tapes/03-change-config.tape b/docs/keyless-demos/tapes/03-change-config.tape new file mode 100644 index 000000000..9905804d2 --- /dev/null +++ b/docs/keyless-demos/tapes/03-change-config.tape @@ -0,0 +1,39 @@ +# Change the configuration of an unclaimed keyless application. +# Preview first, then apply, then read it back from the API. + +Output ../out/03-change-config.gif +Output ../out/03-change-config.mp4 + +Source "_common.tape" + +Hide +Type "source ../lib/demo-env.sh && quiet_shell && new_project change-config && keyless_keys && clear" Enter +Show + +Sleep 1s +Type "# --dry-run diffs locally and sends nothing" +Enter +Type `clerk config patch --json '{"instance":{"support_email":"help@acme.dev"}}' --dry-run` +Sleep 700ms +Enter +Sleep 8s + +Type `clerk config patch --json '{"instance":{"support_email":"help@acme.dev"}}' --yes` +Sleep 700ms +Enter +Sleep 9s + +Type "# read it back — this is the API talking, not a local echo" +Enter +Type "clerk config pull --keys instance" +Sleep 500ms +Enter +Sleep 8s + +Type "# several groups in one payload" +Enter +Type `clerk config patch --json '{"organization_settings":{"max_allowed_memberships":25},"protect":{"rules_enabled":true}}' --yes` +Sleep 700ms +Enter +Sleep 10s +Sleep 2s diff --git a/docs/keyless-demos/tapes/04-toggles.tape b/docs/keyless-demos/tapes/04-toggles.tape new file mode 100644 index 000000000..d0f6222bc --- /dev/null +++ b/docs/keyless-demos/tapes/04-toggles.tape @@ -0,0 +1,29 @@ +# Turn features on and off on an unclaimed keyless application. + +Output ../out/04-toggles.gif +Output ../out/04-toggles.mp4 + +Source "_common.tape" + +Hide +Type "source ../lib/demo-env.sh && quiet_shell && new_project toggles && keyless_keys && clear" Enter +Show + +Sleep 1s +Type "clerk enable orgs" +Sleep 500ms +Enter +Sleep 8s + +Type "# running it again is a no-op, not an error" +Enter +Type "clerk enable orgs" +Sleep 500ms +Enter +Sleep 7s + +Type "clerk disable orgs" +Sleep 500ms +Enter +Sleep 8s +Sleep 2s diff --git a/docs/keyless-demos/tapes/05-templates.tape b/docs/keyless-demos/tapes/05-templates.tape new file mode 100644 index 000000000..ba274b172 --- /dev/null +++ b/docs/keyless-demos/tapes/05-templates.tape @@ -0,0 +1,25 @@ +# Pre-configure the keyless application at creation time with --template. +# A b2b-saas app comes back with organizations already switched on. + +Output ../out/05-templates.gif +Output ../out/05-templates.mp4 + +Source "_common.tape" + +Hide +Type "source ../lib/demo-env.sh && quiet_shell && new_project templates && clear" Enter +Show + +Sleep 1s +Type "clerk init --framework next --keyless --template b2b-saas --yes --no-skills" +Sleep 700ms +Enter +Sleep 32s + +Type "# organizations were enabled before the first key was ever used" +Enter +Type "clerk config pull --keys organization_settings" +Sleep 500ms +Enter +Sleep 9s +Sleep 2s diff --git a/docs/keyless-demos/tapes/06-guardrails.tape b/docs/keyless-demos/tapes/06-guardrails.tape new file mode 100644 index 000000000..f7549c17f --- /dev/null +++ b/docs/keyless-demos/tapes/06-guardrails.tape @@ -0,0 +1,33 @@ +# The honest edges. Everything keyless mode cannot do says so plainly, +# names the reason, and tells you the one command that lifts the limit. + +Output ../out/06-guardrails.gif +Output ../out/06-guardrails.mp4 + +Source "_common.tape" + +Hide +Type "source ../lib/demo-env.sh && quiet_shell && new_project guardrails && keyless_keys && clear" Enter +Show + +Sleep 1s +Type `clerk config put --json '{"instance":{}}' --yes` +Sleep 500ms +Enter +Sleep 6s + +Type `clerk config patch --json '{"session":{"lifetime":3600}}' --yes` +Sleep 500ms +Enter +Sleep 7s + +Type `clerk config patch --instance prod --json '{"instance":{}}' --yes` +Sleep 500ms +Enter +Sleep 6s + +Type "clerk enable billing" +Sleep 500ms +Enter +Sleep 6s +Sleep 2s diff --git a/docs/keyless-demos/tapes/07-agent-mode.tape b/docs/keyless-demos/tapes/07-agent-mode.tape new file mode 100644 index 000000000..a05691e36 --- /dev/null +++ b/docs/keyless-demos/tapes/07-agent-mode.tape @@ -0,0 +1,35 @@ +# The same keyless application, driven the way an AI agent drives it: +# `--mode agent` turns every answer into JSON on stdout. + +Output ../out/07-agent-mode.gif +Output ../out/07-agent-mode.mp4 + +Source "_common.tape" + +Hide +Type "source ../lib/demo-env.sh && quiet_shell && new_project agent-mode && keyless_keys && clear" Enter +Show + +Sleep 1s +Type "clerk --mode agent whoami" +Sleep 500ms +Enter +Sleep 7s + +Type "clerk --mode agent config pull --keys organization_settings | jq '.organization_settings.enabled'" +Sleep 500ms +Enter +Sleep 7s + +Type "clerk --mode agent enable orgs" +Sleep 500ms +Enter +Sleep 8s + +Type "# and the failures are JSON too, so an agent can branch on them" +Enter +Type `clerk --mode agent config patch --json '{"nope":{}}'` +Sleep 500ms +Enter +Sleep 7s +Sleep 2s diff --git a/docs/keyless-demos/tapes/08-sso-connections.tape b/docs/keyless-demos/tapes/08-sso-connections.tape new file mode 100644 index 000000000..796fb5320 --- /dev/null +++ b/docs/keyless-demos/tapes/08-sso-connections.tape @@ -0,0 +1,37 @@ +# Enterprise SSO on an unclaimed keyless application — add one, then remove it. +# +# Social providers (Google, GitHub) live only in the account-level config +# document, so they genuinely need a claimed app. Enterprise SSO connections do +# not: they are their own Backend API resource, and the instance's own secret +# key reaches them. + +Output ../out/08-sso-connections.gif +Output ../out/08-sso-connections.mp4 + +Source "_common.tape" + +Hide +Type "source ../lib/demo-env.sh && quiet_shell && new_project sso && keyless_keys && clear" Enter +Show + +Sleep 1s +Type "clerk api ls enterprise" +Sleep 500ms +Enter +Sleep 7s + +Type `clerk api /enterprise_connections -X POST --yes -d '{"name":"Acme Okta","domains":["acme.test"],"provider":"saml_custom"}' | jq '{id, name, provider, domains}'` +Sleep 700ms +Enter +Sleep 10s + +Type "ID=$(clerk api /enterprise_connections | jq -r '.data[0].id') && echo $ID" +Sleep 500ms +Enter +Sleep 7s + +Type "clerk api /enterprise_connections/$ID -X DELETE --yes" +Sleep 500ms +Enter +Sleep 8s +Sleep 2s diff --git a/docs/keyless-demos/tapes/09-health-and-users.tape b/docs/keyless-demos/tapes/09-health-and-users.tape new file mode 100644 index 000000000..5003af861 --- /dev/null +++ b/docs/keyless-demos/tapes/09-health-and-users.tape @@ -0,0 +1,35 @@ +# Everything else a keyless project should be able to do: check its own health, +# reach its own users, and find its way to the dashboard. +# +# Each of these used to fail on a perfectly working keyless application. + +Output ../out/09-health-and-users.gif +Output ../out/09-health-and-users.mp4 + +Source "_common.tape" + +Hide +# Bootstrapped through `clerk init` rather than by dropping keys in, so the +# project has the claim token init leaves behind — which is what `open` needs. +Type "source ../lib/demo-env.sh && quiet_shell && new_project health" Enter +Type "clerk init --framework next --keyless --yes --no-skills" Enter +Sleep 40s +Type "clear" Enter +Show + +Sleep 1s +Type "clerk doctor" +Sleep 500ms +Enter +Sleep 14s + +Type "clerk users list" +Sleep 500ms +Enter +Sleep 8s + +Type "clerk open --print" +Sleep 500ms +Enter +Sleep 8s +Sleep 2s diff --git a/docs/keyless-demos/tapes/_common.tape b/docs/keyless-demos/tapes/_common.tape new file mode 100644 index 000000000..25845a4c7 --- /dev/null +++ b/docs/keyless-demos/tapes/_common.tape @@ -0,0 +1,19 @@ +# Shared look and feel for every keyless demo. +# +# Sourced by each tape with `Source _common.tape`. Recording settings only — +# no commands — so a tape stays a readable script of what is being shown. + +Require clerk + +Set Shell bash +Set FontSize 16 +Set Width 1400 +Set Height 860 +Set Padding 28 +Set Margin 24 +Set MarginFill "#1b1a26" +Set BorderRadius 12 +Set WindowBar Colorful +Set Framerate 12 +Set Theme "Catppuccin Mocha" +Set TypingSpeed 28ms diff --git a/packages/cli-core/src/commands/api/README.md b/packages/cli-core/src/commands/api/README.md index cffc0d4d4..1ddb6b500 100644 --- a/packages/cli-core/src/commands/api/README.md +++ b/packages/cli-core/src/commands/api/README.md @@ -5,6 +5,11 @@ Make authenticated HTTP requests to Clerk APIs directly from the command line. By default, targets the Clerk Backend API (`https://api.clerk.dev/v1/`) using the instance secret key. Use `--platform` to target the Platform API instead. +Works with no login and no linked project on an **unclaimed keyless +application** — the one an SDK creates for itself the first time you run +`next dev` (or similar) with no keys configured — by reading the secret key it +already left on disk. See [Authentication](#authentication) below. + ## Usage ```sh @@ -81,14 +86,19 @@ clerk api --fapi /environment --app app_123 --instance dev Secret key resolution order (Backend API, the default): 1. `--secret-key` flag (explicit) -2. `CLERK_SECRET_KEY` environment variable -3. Auto-resolve from `--app ` via the Platform API (see below) +2. Auto-resolve from `--app ` via the Platform API (see below) +3. This project's own keyless secret key — from `CLERK_SECRET_KEY`, `.env.local` + (or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json` 4. Auto-resolve from linked project profile via the Platform API (see below) -Steps 3 and 4 both exchange a Platform API token for the target instance's -secret key, so either needs Platform API auth to be available. Step 3 works -from any directory (no `clerk link` required); step 4 uses the app ID stored -by `clerk link`. +Step 2 exchanges a Platform API token for the target instance's secret key and +works from any directory (no `clerk link` required). Step 3 is what makes +`clerk api` work out of the box against an **unclaimed keyless application** — +the one an SDK creates for itself on first `next dev` (or similar) with no keys +configured — with no login and no Platform API auth at all; it only applies when +the directory isn't linked and `--app` wasn't passed, since either of those names +an explicit destination the on-disk key might not belong to. Step 4 uses the app +ID stored by `clerk link` and needs Platform API auth like step 2. Platform API auth (used by `--platform` mode, and by steps 3 and 4 above): diff --git a/packages/cli-core/src/commands/config/README.md b/packages/cli-core/src/commands/config/README.md index f5f10d164..fe28631b7 100644 --- a/packages/cli-core/src/commands/config/README.md +++ b/packages/cli-core/src/commands/config/README.md @@ -211,7 +211,7 @@ clerk config patch --json '{ | `oauth_application_settings` | `/v1/instance/oauth_application_settings` | Yes | Dynamic OAuth client registration | | `instance_settings` | `/v1/beta_features/instance_settings` | No | `test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist` | -Any other top-level key exits with a usage error naming the supported ones — the account-mode keys (`session`, `sign_up`, `auth_email`, …) are not silently translated. +Any other top-level key exits with a usage error naming the supported ones. Most of them (`session`, `sign_up`, `auth_email`, …) are genuinely account-only and the error points at `clerk auth login`. A handful — `enterprise_connections`, `saml_connections`, `oauth_applications`, `domains` — are BAPI resource _collections_ reachable on an unclaimed application today; they're just not part of this config document, so the error points at `clerk api /` instead of a login that wouldn't add them anyway. `instance_settings` is backed by a beta route, and is the only way to reach those four auth-config fields without an account — which is why it's included. @@ -219,15 +219,28 @@ Any other top-level key exits with a usage error naming the supported ones — t `GET /v1/instance` returns a subset of what `PATCH /v1/instance` accepts — `support_email`, for example, is writable but not readable. Fields the read omits have no "before" value to compare against, so they always appear as additions in the diff and never trigger "No changes detected". The write itself is unaffected: patching a field to the value it already holds is a no-op server-side. +### Round-trip verification + +A 200 or 204 from a keyless write only means Clerk's Backend API accepted the request — it silently drops fields it doesn't recognize inside a group instead of rejecting them, and at least one route (`PATCH /v1/instance` with `allowed_origins: null` or `[]`) accepts a value it then ignores. Printing "Config pushed successfully" off the HTTP status alone would paper over both. + +After a write, the CLI checks every field it sent against the PATCH response body, and against nothing else. Fields whose value round-trips are reported as applied; fields the response doesn't reflect are named explicitly instead of folded into an unconditional success line. + +A follow-up GET looks like stronger evidence and is in fact weaker. BAPI omits writable-but-not-readable fields from its reads — `instance.support_email` is accepted and never echoed — and reads are eventually consistent, so a GET issued straight after a write routinely returns the pre-write value. Verifying against one reports perfectly good writes as dropped. This applies equally to the six groups that do have a GET route: the response body is the only read that is guaranteed to be about _this_ write. + +That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothing can confirm it after the fact, so the check moves to before the request instead: the fields that route accepts are a closed set in BAPI's own schema (`additionalProperties: false`), and `assertKeylessPayload` rejects anything outside it. This matters because that route is also the one people reach for when trying to enable password auth or a social provider — none of which it accepts, and all of which it used to swallow with a `204` and a success message. The group is still reported as unconfirmed, and contributes no state to the printed envelope rather than a possibly-stale re-read. + +`restrictions` and `instance_settings` have no GET route, but both echo their new state in the PATCH response, so their writes verify normally. + ### Differences from account mode -| Behavior | Account mode | Keyless mode | -| --------------- | ---------------------------------------- | ----------------------------------------------------------------- | -| Coverage | Full config document | Three Backend API resources | -| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance | -| `--dry-run` | Server-side validation of the projection | Local diff only; nothing is sent | -| `config put` | Replaces the whole document | Errors — no full document exists to replace | -| `config schema` | Returns the JSON Schema | Errors — the schema describes the account-level document | +| Behavior | Account mode | Keyless mode | +| ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | +| Coverage | Full config document | Seven Backend API resources | +| `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance | +| `--dry-run` | Server-side validation of the projection | Local diff only; nothing is sent | +| `config put` | Replaces the whole document | Errors — no full document exists to replace | +| `config schema` | Returns the JSON Schema | Errors — the schema describes the account-level document | +| Write confirmation | Trusts the response body outright | Verifies each sent field round-tripped and names what couldn't be confirmed (see [Round-trip verification](#round-trip-verification)) | Run `clerk auth login` to claim the application; auto-claim links it, and every config command then uses account mode with full coverage. @@ -235,10 +248,17 @@ Run `clerk auth login` to claim the application; auto-claim links it, and every All requests go to the Clerk Backend API (default `https://api.clerk.dev`, overridable via `CLERK_BACKEND_API_URL`), authenticated with a `Bearer sk_…` instance secret key. -| Method | Endpoint | Description | -| ------- | ------------------------------------ | ------------------------------------------------------------------------------------------ | -| `GET` | `/v1/instance` | Reads instance settings for `config pull` and for the pre-write diff. | -| `PATCH` | `/v1/instance` | Updates instance settings. Answers `204` with no body, so the group is re-read afterwards. | -| `PATCH` | `/v1/instance/restrictions` | Updates sign-up restrictions (allowlist, blocklist). Write-only — no read route exists. | -| `GET` | `/v1/instance/organization_settings` | Reads organization settings for `config pull` and for the pre-write diff. | -| `PATCH` | `/v1/instance/organization_settings` | Updates organization settings. | +| Method | Endpoint | Description | +| ------- | ----------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `GET` | `/v1/instance` | Reads instance settings for `config pull` and for the pre-write diff. | +| `PATCH` | `/v1/instance` | Updates instance settings. Answers `204` with no body, so the write can't be confirmed afterwards — field names are validated against BAPI's schema beforehand instead. | +| `GET` | `/v1/instance/communication` | Reads communication settings (e.g. blocked country codes) for `config pull` and for the pre-write diff. | +| `PATCH` | `/v1/instance/communication` | Updates communication settings. | +| `PATCH` | `/v1/instance/restrictions` | Updates sign-up restrictions (allowlist, blocklist). No read route, but the response echoes the new state, so the write verifies normally. | +| `GET` | `/v1/instance/organization_settings` | Reads organization settings for `config pull` and for the pre-write diff. | +| `PATCH` | `/v1/instance/organization_settings` | Updates organization settings. | +| `GET` | `/v1/instance/protect` | Reads bot-protection settings for `config pull` and for the pre-write diff. | +| `PATCH` | `/v1/instance/protect` | Updates bot-protection settings. | +| `GET` | `/v1/instance/oauth_application_settings` | Reads dynamic OAuth client registration settings for `config pull` and for the pre-write diff. | +| `PATCH` | `/v1/instance/oauth_application_settings` | Updates dynamic OAuth client registration settings. | +| `PATCH` | `/v1/beta_features/instance_settings` | Updates `test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist`. No read route, but the response echoes the new state. | diff --git a/packages/cli-core/src/commands/config/apply-patch.ts b/packages/cli-core/src/commands/config/apply-patch.ts index 2ee195819..b46d34139 100644 --- a/packages/cli-core/src/commands/config/apply-patch.ts +++ b/packages/cli-core/src/commands/config/apply-patch.ts @@ -3,7 +3,7 @@ import { withSpinner } from "../../lib/spinner.ts"; import { confirm } from "../../lib/prompts.ts"; import { isHuman } from "../../mode.ts"; import { log } from "../../lib/log.ts"; -import { hasConfigChanges, printDiff } from "./push.ts"; +import { hasConfigChanges, printDiff, reportWriteOutcome } from "./push.ts"; import type { InstanceTarget } from "../../lib/keyless-target.ts"; import { assertPayloadWritable, @@ -68,7 +68,11 @@ export async function applyConfigPatch(opts: ApplyPatchOptions): Promise; + /** + * Only present for a keyless write. An account write's response body IS the + * config document, trusted outright; a keyless write only gets a 200/204 + * meaning "request accepted", so what actually landed is checked separately. + */ + verification?: KeylessWriteVerification; +} + /** * Rejects a payload the target can't accept, before any diff is printed or any * prompt is shown. Only the keyless path constrains the payload — the account @@ -55,7 +71,7 @@ export function supportsServerDryRun(target: InstanceTarget): boolean { export const LOCAL_DRY_RUN_MESSAGE = "[dry-run] Nothing sent — no changes applied"; -export function writeInstanceConfig( +export async function writeInstanceConfig( target: InstanceTarget, payload: Record, options: { @@ -64,20 +80,25 @@ export function writeInstanceConfig( dryRun?: boolean; failureContext: string; }, -): Promise> { +): Promise { if (target.kind === "keyless") { // PUT is rejected before reaching here: there is no full document to // replace when the instance is addressed by its own key. The payload was // validated by `assertPayloadWritable` before the diff was shown. - return patchKeylessConfig(target.keyless, payload as Record>); + const { applied, verification } = await patchKeylessConfig( + target.keyless, + payload as Record>, + ); + return { body: applied, verification }; } const apiFn = options.method === "PUT" ? putInstanceConfig : patchInstanceConfig; - return withApiContext( + const body = await withApiContext( apiFn(target.ctx.appId, target.ctx.instanceId, payload, { destructive: options.destructive, dryRun: options.dryRun, }), options.dryRun ? "Dry-run failed" : options.failureContext, ); + return { body }; } diff --git a/packages/cli-core/src/commands/config/keyless.test.ts b/packages/cli-core/src/commands/config/keyless.test.ts index da37a9b94..e85f830c2 100644 --- a/packages/cli-core/src/commands/config/keyless.test.ts +++ b/packages/cli-core/src/commands/config/keyless.test.ts @@ -54,6 +54,9 @@ describe("keyless config", () => { let exitSpy: ReturnType; const captured = useCaptureLog(); + /** Mutable per-test state so a PATCH's effect actually shows up on the next GET/re-read. */ + let bapiState: Record; + beforeEach(async () => { tempDir = await mkdtemp(join(tmpdir(), "clerk-keyless-config-")); projectDir = await mkdtemp(join(tmpdir(), "clerk-keyless-project-")); @@ -66,12 +69,22 @@ describe("keyless config", () => { throw new Error("process.exit"); }); + bapiState = structuredClone(READABLE_BODIES); + stubFetch(async (input, init) => { const path = input.toString().replace(BAPI_URL, ""); const method = (init?.method ?? "GET").toUpperCase(); - // PATCH /v1/instance answers 204 with no body; every other group echoes. + if (method === "GET") { + const body = bapiState[path]; + if (body) return new Response(JSON.stringify(body), { status: 200 }); + throw new Error(`Unexpected fetch: ${method} ${path}`); + } + + // PATCH /v1/instance answers 204 with no body but does persist the + // fields it accepts, so a re-read after it reflects the write. if (path === "/v1/instance" && method === "PATCH") { + bapiState[path] = { ...(bapiState[path] as object), ...JSON.parse(init?.body as string) }; return new Response(null, { status: 204 }); } if (path === "/v1/instance/restrictions" && method === "PATCH") { @@ -84,8 +97,10 @@ describe("keyless config", () => { status: 200, }); } - const body = READABLE_BODIES[path]; - if (body) return new Response(JSON.stringify(body), { status: 200 }); + if (method === "PATCH" && bapiState[path]) { + bapiState[path] = { ...(bapiState[path] as object), ...JSON.parse(init?.body as string) }; + return new Response(JSON.stringify(bapiState[path]), { status: 200 }); + } throw new Error(`Unexpected fetch: ${method} ${path}`); }); @@ -203,7 +218,7 @@ describe("keyless config", () => { expect(await resolveKeylessTarget({ app: "app_1", cwd: projectDir })).toBeUndefined(); }); - test("resolves even when a platform API key is set, and says why", async () => { + test("resolves even when a platform API key is set", async () => { await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform"; const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); @@ -212,11 +227,15 @@ describe("keyless config", () => { secretKey: SECRET_KEY, source: ".env", }); - expect(captured.err).toContain("isn't linked to an application"); }); - test("stays quiet about the reduced coverage when there is no account", async () => { + // The reduced-coverage warning belongs to the config surface, not to + // resolution — `whoami`, `open` and `doctor` want the same keyless answer + // whether or not an account exists, and a resolver that warns is one a + // diagnostic tool can't call without polluting its own report. + test("stays quiet about reduced coverage — that warning is the config surface's", async () => { await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform"; const { resolveKeylessTarget } = await import("../../lib/keyless-target.ts"); await resolveKeylessTarget({ cwd: projectDir }); @@ -224,6 +243,26 @@ describe("keyless config", () => { expect(captured.err).not.toContain("isn't linked to an application"); }); + test("resolveInstanceTarget is the one that says the keyless view covers less", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform"; + const { resolveInstanceTarget } = await import("../../lib/keyless-target.ts"); + + const target = await resolveInstanceTarget({ cwd: projectDir }); + + expect(target.kind).toBe("keyless"); + expect(captured.err).toContain("isn't linked to an application"); + }); + + test("says nothing about linking when there is no account at all", async () => { + await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); + const { resolveInstanceTarget } = await import("../../lib/keyless-target.ts"); + + await resolveInstanceTarget({ cwd: projectDir }); + + expect(captured.err).not.toContain("isn't linked to an application"); + }); + test("defers to the account path when the project is linked", async () => { await writeEnv(".env", `CLERK_SECRET_KEY=${SECRET_KEY}\n`); await setProfile(projectDir, { @@ -275,10 +314,75 @@ describe("keyless config", () => { ); }); + test("points account-only keys at `clerk auth login`", async () => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ session: { lifetime: 10 } })).toThrow( + /Run `clerk auth login` to claim the application/, + ); + }); + + // enterprise_connections/saml_connections/oauth_applications/domains are + // BAPI resource collections reachable on an unclaimed application today — + // verified live via `clerk api /enterprise_connections`. Telling the user + // to claim the app for these would be a detour to nowhere: claiming + // doesn't add them to the config document either. + test("points BAPI resource collections at `clerk api` instead of login", async () => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ enterprise_connections: {} })).toThrow( + /use `clerk api \/enterprise_connections` directly instead of this config document/, + ); + }); + + test("does not suggest `clerk auth login` for a BAPI resource collection", async () => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ domains: {} })).not.toThrow(/clerk auth login/); + }); + test("rejects a group whose value is not an object", async () => { const { assertKeylessPayload } = await import("./keyless.ts"); expect(() => assertKeylessPayload({ instance: "nope" })).toThrow(/must be a JSON object/); }); + + // `PATCH /v1/instance` answers 204 and drops field names it doesn't know, + // so an unrecognised field there is invisible in the response — the only + // place it can be caught is before the request goes out. + test("rejects an `instance` field the Backend API would silently drop", async () => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ instance: { suport_email: "a@b.com" } })).toThrow( + /Unsupported field on `instance`.*suport_email/s, + ); + }); + + test.each([ + ["password", "which authentication strategies are enabled"], + ["social", "social sign-in providers"], + ["second_factors", "multi-factor authentication policy"], + ])("explains that `instance.%s` has no Backend API route at all", async (field, reason) => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ instance: { [field]: true } })).toThrow( + new RegExp(`no route for ${reason}`), + ); + }); + + test.each([ + "test_mode", + "hibp", + "support_email", + "clerk_js_version", + "development_origin", + "allowed_origins", + "cookieless_dev", + "url_based_session_syncing", + "preferred_sign_in_strategy_when_password_required", + ])("accepts the documented `instance` field %s", async (field) => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ instance: { [field]: "x" } })).not.toThrow(); + }); + + test("leaves fields on other groups alone — their writes echo back", async () => { + const { assertKeylessPayload } = await import("./keyless.ts"); + expect(() => assertKeylessPayload({ protect: { nonsense_field: true } })).not.toThrow(); + }); }); describe("pullKeylessConfig", () => { @@ -319,19 +423,20 @@ describe("keyless config", () => { }); describe("patchKeylessConfig", () => { - test("sends each group to its own endpoint", async () => { + test("sends each group to its own endpoint and confirms the fields landed", async () => { const requests: string[] = []; stubFetch(async (input, init) => { - const url = input.toString(); + const path = input.toString().replace(BAPI_URL, ""); const method = (init?.method ?? "GET").toUpperCase(); - requests.push(`${method} ${url.replace(BAPI_URL, "")}`); - if (url.endsWith("/v1/instance") && method === "PATCH") { + requests.push(`${method} ${path}`); + if (path === "/v1/instance" && method === "PATCH") { + bapiState[path] = { ...(bapiState[path] as object), ...JSON.parse(init?.body as string) }; return new Response(null, { status: 204 }); } - if (url.endsWith("/v1/instance")) { - return new Response(JSON.stringify(INSTANCE), { status: 200 }); - } - return new Response(JSON.stringify(ORG_SETTINGS), { status: 200 }); + if (method === "GET") return new Response(JSON.stringify(bapiState[path]), { status: 200 }); + const updated = { ...(bapiState[path] as object), ...JSON.parse(init?.body as string) }; + bapiState[path] = updated; + return new Response(JSON.stringify(updated), { status: 200 }); }); const { patchKeylessConfig } = await import("./keyless.ts"); @@ -343,13 +448,115 @@ describe("keyless config", () => { }, ); - expect(requests).toEqual([ - "PATCH /v1/instance", - // 204 carries no body, so the group is re-read for the caller. - "GET /v1/instance", - "PATCH /v1/instance/organization_settings", - ]); - expect(result).toEqual({ instance: INSTANCE, organization_settings: ORG_SETTINGS }); + expect(requests).toEqual(["PATCH /v1/instance", "PATCH /v1/instance/organization_settings"]); + // `instance` answered 204, so it contributes no state — deliberately + // absent rather than re-read, because that read is eventually consistent + // and would show the pre-write value under a success message. + expect(result.applied).toEqual({ + organization_settings: { ...ORG_SETTINGS, enabled: true }, + }); + expect(result.verification).toEqual({ + verifiedFields: ["organization_settings.enabled"], + droppedFields: [], + unverifiableGroups: ["instance"], + }); + }); + + test("never re-reads a group whose write answered with no body", async () => { + // Regression: the re-read this asserts against returned the pre-write + // value often enough that `Config pushed successfully` was printed + // directly above stale state, reading as though nothing had been applied. + const requests: string[] = []; + stubFetch(async (input, init) => { + const method = (init?.method ?? "GET").toUpperCase(); + requests.push(`${method} ${input.toString().replace(BAPI_URL, "")}`); + return new Response(null, { status: 204 }); + }); + const { patchKeylessConfig } = await import("./keyless.ts"); + + const result = await patchKeylessConfig( + { secretKey: SECRET_KEY, source: ".env" }, + { instance: { support_email: "new@example.com" } }, + ); + + expect(requests).toEqual(["PATCH /v1/instance"]); + expect(result.applied).toEqual({}); + expect(result.verification.unverifiableGroups).toEqual(["instance"]); + expect(result.verification.droppedFields).toEqual([]); + }); + + test("names an unconfirmed group as already applied when a later group fails", async () => { + // `instance` leaves no trace in the envelope, but it did land — a failure + // downstream still has to say so or the user can't tell how far it got. + stubFetch(async (input) => { + const path = input.toString().replace(BAPI_URL, ""); + if (path === "/v1/instance") return new Response(null, { status: 204 }); + return new Response(JSON.stringify({ errors: [{ message: "nope" }] }), { status: 500 }); + }); + const { patchKeylessConfig } = await import("./keyless.ts"); + + // `withApiContext` attaches the context to the error rather than to its + // message; the global handler prints the two together. + const error = await patchKeylessConfig( + { secretKey: SECRET_KEY, source: ".env" }, + { instance: { support_email: "new@example.com" }, protect: { rules_enabled: true } }, + ).catch((thrown: unknown) => thrown); + + expect((error as { context?: string }).context).toBe( + "Failed to update protect (already applied: instance)", + ); + }); + + test("reports a field the API silently dropped instead of claiming it landed", async () => { + // A typo'd field: BAPI's PATCH routes ignore unknown fields inside a + // group rather than rejecting them, so the request answers 200 with the + // resource as it actually stands — without the typo'd key. + stubFetch(async () => new Response(JSON.stringify(PROTECT), { status: 200 })); + const { patchKeylessConfig } = await import("./keyless.ts"); + + const result = await patchKeylessConfig( + { secretKey: SECRET_KEY, source: ".env" }, + { protect: { rules_enabledx: true } as Record }, + ); + + expect(result.verification).toEqual({ + verifiedFields: [], + droppedFields: ["protect.rules_enabledx"], + unverifiableGroups: [], + }); + }); + + // The whole point of checking the PATCH response and not a follow-up read. + // `instance.support_email` is writable but never echoed by `GET /v1/instance`, + // and BAPI's reads are eventually consistent — verifying against a re-read + // reported both as dropped when the write had in fact landed. + test("never calls a successful instance write dropped just because the read can't show it", async () => { + stubFetch(async (input, init) => { + const path = input.toString().replace(BAPI_URL, ""); + const method = (init?.method ?? "GET").toUpperCase(); + if (path === "/v1/instance" && method === "PATCH") + return new Response(null, { status: 204 }); + // The read never carries support_email, and still shows the pre-write + // allowed_origins — exactly what the real API does. + return new Response( + JSON.stringify({ ...INSTANCE, allowed_origins: ["https://stale.example.com"] }), + { status: 200 }, + ); + }); + const { patchKeylessConfig } = await import("./keyless.ts"); + + const result = await patchKeylessConfig( + { secretKey: SECRET_KEY, source: ".env" }, + { + instance: { + support_email: "new@example.com", + allowed_origins: ["https://fresh.example.com"], + }, + }, + ); + + expect(result.verification.droppedFields).toEqual([]); + expect(result.verification.unverifiableGroups).toEqual(["instance"]); }); test("applies groups in table order, not payload order", async () => { @@ -396,7 +603,10 @@ describe("keyless config", () => { ).rejects.toMatchObject({ context: "Failed to update protect (already applied: instance)" }); }); - test("returns the response body for groups that answer with one", async () => { + // A group having no GET route doesn't make its write unverifiable: what + // the PATCH itself answers with is the resource as it now stands, which is + // the only evidence the check ever uses. + test("verifies a write-only group from the body its own PATCH returns", async () => { const { patchKeylessConfig } = await import("./keyless.ts"); const result = await patchKeylessConfig( @@ -404,9 +614,14 @@ describe("keyless config", () => { { restrictions: { allowlist: true } }, ); - expect(result).toEqual({ + expect(result.applied).toEqual({ restrictions: { object: "instance_restrictions", allowlist: true }, }); + expect(result.verification).toEqual({ + verifiedFields: ["restrictions.allowlist"], + droppedFields: [], + unverifiableGroups: [], + }); }); }); @@ -483,7 +698,11 @@ describe("keyless config", () => { const path = input.toString().replace(BAPI_URL, ""); const method = (init?.method ?? "GET").toUpperCase(); requests.push(`${method} ${path}`); - return new Response(JSON.stringify(READABLE_BODIES[path] ?? ORG_SETTINGS), { status: 200 }); + if (method === "PATCH") { + // Reflect the write on the next read so the round-trip check confirms it. + bapiState[path] = { ...(bapiState[path] as object), ...JSON.parse(init?.body as string) }; + } + return new Response(JSON.stringify(bapiState[path] ?? ORG_SETTINGS), { status: 200 }); }); const { orgsEnable } = await import("../orgs/index.ts"); diff --git a/packages/cli-core/src/commands/config/keyless.ts b/packages/cli-core/src/commands/config/keyless.ts index e13985082..9ff12dac9 100644 --- a/packages/cli-core/src/commands/config/keyless.ts +++ b/packages/cli-core/src/commands/config/keyless.ts @@ -50,6 +50,93 @@ function isReadable(name: KeylessGroup): boolean { return KEYLESS_GROUPS[name].readable; } +/** + * Top-level keys that aren't a config group but ARE reachable on an unclaimed + * keyless application — BAPI resource collections with their own routes, + * verified live (`clerk api /enterprise_connections` lists and creates them + * with just an instance secret key). Naming these in the "claim the app" + * advice would send someone on a detour they don't need: `clerk api` already + * works, and claiming wouldn't add them to the config document either. + */ +const API_REACHABLE_KEYLESS_KEYS = new Set([ + "enterprise_connections", + "saml_connections", + "oauth_applications", + "domains", + "allowlist_identifiers", + "blocklist_identifiers", +]); + +/** + * Every field `PATCH /v1/instance` accepts, from the Backend API's own schema + * (`additionalProperties: false`). + * + * This is the one group whose write can't be checked against anything: it + * answers 204 with no body, so an unrecognised field name is indistinguishable + * from an applied one — BAPI drops what it doesn't know rather than rejecting + * it. Naming the fields here moves that failure forward to a usage error, and + * is worth the maintenance precisely because the alternative is silent. Sending + * `{"instance": {"password": "on"}}` used to report success and change nothing. + * + * Every other group echoes its new state back, so a typo there already surfaces + * as a dropped field and needs no list. + */ +const INSTANCE_FIELDS = new Set([ + "test_mode", + "hibp", + "support_email", + "clerk_js_version", + "development_origin", + "allowed_origins", + "cookieless_dev", + "url_based_session_syncing", + "preferred_sign_in_strategy_when_password_required", +]); + +/** + * Auth settings people reach for on the `instance` group that BAPI has no route + * for at all, mapped to the reason. Worth naming individually: "unsupported + * field" reads like a typo, and someone who just tried to turn on GitHub sign-in + * deserves to know no amount of retyping will do it. + */ +const ACCOUNT_ONLY_INSTANCE_FIELDS: Record = { + password: "which authentication strategies are enabled", + phone_number: "which authentication strategies are enabled", + username: "which authentication strategies are enabled", + email_address: "which authentication strategies are enabled", + passkey: "which authentication strategies are enabled", + social: "social sign-in providers", + oauth: "social sign-in providers", + second_factors: "multi-factor authentication policy", + application_name: "the application's name and branding", +}; + +/** + * Rejects `instance` fields BAPI would silently discard, before the request is + * sent. Runs only for that group — see `INSTANCE_FIELDS`. + */ +function assertInstanceFields(fields: Record): void { + const unknown = Object.keys(fields).filter((field) => !INSTANCE_FIELDS.has(field)); + if (unknown.length === 0) return; + + const lines = [ + `Unsupported ${unknown.length === 1 ? "field" : "fields"} on \`instance\` for an unclaimed keyless application: ${unknown.join(", ")}.`, + `Supported fields: ${[...INSTANCE_FIELDS].join(", ")}.`, + ]; + + // Say why, once per distinct reason, for the fields people actually try. + const reasons = [ + ...new Set(unknown.map((field) => ACCOUNT_ONLY_INSTANCE_FIELDS[field]).filter(Boolean)), + ]; + for (const reason of reasons) { + lines.push( + `Clerk's Backend API has no route for ${reason}, so this can't be changed from an unclaimed application at all — claim it first with \`clerk auth login\`.`, + ); + } + + throwUsageError(lines.join("\n")); +} + /** * Validates caller-supplied names once, at the boundary, so everything * downstream works with a known group instead of re-checking strings. @@ -71,11 +158,30 @@ export function assertKeylessPayload( ): asserts payload is Record> { const unknown = Object.keys(payload).filter((key) => !(key in KEYLESS_GROUPS)); if (unknown.length > 0) { - throwUsageError( - `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed keyless application: ${unknown.join(", ")}.\n` + - `Supported top-level keys: ${KEYLESS_GROUP_NAMES.join(", ")}.\n` + + const apiReachable = unknown.filter((key) => API_REACHABLE_KEYLESS_KEYS.has(key)); + const accountOnly = unknown.filter((key) => !API_REACHABLE_KEYLESS_KEYS.has(key)); + + const lines = [ + `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed keyless application: ${unknown.join(", ")}.`, + `Supported top-level keys: ${KEYLESS_GROUP_NAMES.join(", ")}.`, + ]; + + // Point these at `clerk api` — they're reachable today, and claiming the + // application wouldn't move them into the config document anyway. + if (apiReachable.length > 0) { + lines.push( + `${apiReachable.join(", ")} ${apiReachable.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use \`clerk api /${apiReachable[0]}\` directly instead of this config document.`, + ); + } + + // Everything left really is part of the account-mode config document. + if (accountOnly.length > 0) { + lines.push( "Run `clerk auth login` to claim the application and use the full config document.", - ); + ); + } + + throwUsageError(lines.join("\n")); } for (const [key, value] of Object.entries(payload)) { @@ -87,6 +193,10 @@ export function assertKeylessPayload( ); } } + + if (payload.instance) { + assertInstanceFields(payload.instance as Record); + } } /** @@ -121,26 +231,97 @@ export async function pullKeylessConfig( return config; } +export interface KeylessWriteVerification { + /** Dotted `group.field` paths confirmed to hold the value that was sent. */ + verifiedFields: string[]; + /** + * Dotted `group.field` paths the API's own response to the write doesn't + * reflect — a 200 there means "request accepted", not "field applied", and + * BAPI silently drops fields it doesn't recognize instead of rejecting them. + */ + droppedFields: string[]; + /** Groups whose write answered with no body, so it can't be confirmed either way. */ + unverifiableGroups: KeylessGroup[]; +} + +export interface KeylessPatchResult { + /** Per-group state as reported back by the API — same envelope shape a pull returns. */ + applied: Record; + verification: KeylessWriteVerification; +} + +/** + * Walks every leaf the caller sent and records whether the observed value + * (the API's own read-back) matches it. `sent` is always an object — payload + * groups are validated by `assertKeylessPayload` before this runs — so only + * `observed` needs a runtime check; a group BAPI dropped won't have it. + */ +function collectVerifiedLeaves( + sent: Record, + observed: unknown, + path: string, + out: { path: string; matched: boolean }[], +): void { + const observedObj = + observed !== null && typeof observed === "object" && !Array.isArray(observed) + ? (observed as Record) + : undefined; + + for (const [key, value] of Object.entries(sent)) { + const fieldPath = path ? `${path}.${key}` : key; + const observedValue = observedObj?.[key]; + + if (value !== null && typeof value === "object" && !Array.isArray(value)) { + collectVerifiedLeaves(value as Record, observedValue, fieldPath, out); + continue; + } + + out.push({ path: fieldPath, matched: JSON.stringify(value) === JSON.stringify(observedValue) }); + } +} + /** * Applies each group in the payload to its own BAPI resource and returns what - * the API reported back. `PATCH /v1/instance` answers 204 with no body, so that - * group is re-read to give the caller something to see. + * the API reported back, plus which of the sent fields that report actually + * confirms landed. + * + * Verification uses the PATCH response body and nothing else. A follow-up GET + * looks like better evidence and isn't: BAPI omits writable-but-not-readable + * fields from its reads (`instance.support_email` is never echoed), and reads + * are eventually consistent, so a GET issued straight after a write can still + * be showing the old value. Either would report a perfectly good write as + * dropped. + * + * `PATCH /v1/instance` answers 204 with no body at all, and that group is + * reported with no state rather than re-read. An earlier version did re-read it + * "so the caller sees something", which turned out to be the worst of both: the + * eventually-consistent GET routinely returned the pre-write value, and printing + * it directly under `Config pushed successfully` read as though the write had + * been ignored. Nothing is more honest than a stale something here — the fields + * that group accepts are validated before the request goes out, so a 204 is + * already good evidence the write landed. */ export async function patchKeylessConfig( target: KeylessTarget, payload: Record>, -): Promise> { +): Promise { const results: Record = {}; + const verifiedFields: string[] = []; + const droppedFields: string[] = []; + const unverifiableGroups: KeylessGroup[] = []; + // Tracked separately from `results`, which only carries groups that returned + // state to show. A group can be applied and still contribute nothing to the + // envelope, and a later failure has to name it regardless. + const appliedGroups: KeylessGroup[] = []; // Each group is its own request and the Backend API has no transaction, so a // failure part-way through leaves earlier groups applied. Name them in the // error rather than letting the user guess how far it got. for (const name of KEYLESS_GROUP_NAMES.filter((group) => group in payload)) { const group = KEYLESS_GROUPS[name]; - const applied = Object.keys(results); const context = - applied.length > 0 - ? `Failed to update ${name} (already applied: ${applied.join(", ")})` + appliedGroups.length > 0 + ? `Failed to update ${name} (already applied: ${appliedGroups.join(", ")})` : `Failed to update ${name}`; const response = await withApiContext( @@ -153,23 +334,28 @@ export async function patchKeylessConfig( context, ); - if (response.body) { - results[name] = response.body; + const written = response.body ?? null; + appliedGroups.push(name); + + if (!written) { + // Nothing came back to check against, and nothing worth inventing: see + // the note above on why the re-read that used to live here was removed. + // The group is left out of the envelope entirely rather than shown as + // null, and the success line names it as unconfirmed. + unverifiableGroups.push(name); continue; } - // A 204 carries nothing to show; re-read so the caller still sees the group. - results[name] = group.readable - ? ( - await withApiContext( - bapiRequest({ method: "GET", path: group.path, secretKey: target.secretKey }), - `Failed to fetch ${name}`, - ) - ).body - : null; + results[name] = written; + + const leaves: { path: string; matched: boolean }[] = []; + collectVerifiedLeaves(payload[name] ?? {}, written, "", leaves); + for (const leaf of leaves) { + (leaf.matched ? verifiedFields : droppedFields).push(`${name}.${leaf.path}`); + } } - return results; + return { applied: results, verification: { verifiedFields, droppedFields, unverifiableGroups } }; } /** Current state of the groups a payload touches, for diffing before a write. */ diff --git a/packages/cli-core/src/commands/config/push.ts b/packages/cli-core/src/commands/config/push.ts index 6dd56f94d..3c55d74d8 100644 --- a/packages/cli-core/src/commands/config/push.ts +++ b/packages/cli-core/src/commands/config/push.ts @@ -13,6 +13,7 @@ import { withSpinner, intro, outro, pausedOutro } from "../../lib/spinner.ts"; import { isInsideGutter, log } from "../../lib/log.ts"; import { NEXT_STEPS, printNextSteps } from "../../lib/next-steps.ts"; import { resolveInstanceTarget } from "../../lib/keyless-target.ts"; +import type { KeylessWriteVerification } from "./keyless.ts"; import { assertPayloadWritable, LOCAL_DRY_RUN_MESSAGE, @@ -124,17 +125,14 @@ async function configPush(options: ConfigPushOptions, op: Operation): Promise 0) { + const [one, them] = + droppedFields.length === 1 ? ["This field", "it"] : ["These fields", "them"]; + log.warn( + `${one} didn't come back in Clerk's Backend API response: ${droppedFields.join(", ")}. ` + + `The API ignores field names it doesn't recognise rather than rejecting them, so check ${them} for a typo against the diff above.`, + ); + } + + // Always close with a success line: the write was accepted, and a run that + // ends on a warning alone reads as a failure that never happened. + log.success( + unverifiableGroups.length > 0 + ? `${successMessage} — ${unverifiableGroups.join(", ")} answered with no body, so ${unverifiableGroups.length === 1 ? "that group" : "those groups"} couldn't be confirmed` + : successMessage, + ); +} diff --git a/packages/cli-core/src/commands/doctor/README.md b/packages/cli-core/src/commands/doctor/README.md index 8eef28a31..8d744ba1d 100644 --- a/packages/cli-core/src/commands/doctor/README.md +++ b/packages/cli-core/src/commands/doctor/README.md @@ -37,6 +37,30 @@ clerk doctor --fix # Offer to auto-fix issues | Shell completion | Configuration | Shell autocompletion is installed for the detected shell | | MCP server | Integration | If a Clerk MCP entry is installed, every distinct configured server answers the `initialize` handshake; warns on an unreadable client config (skipped when nothing is installed; warns, never fails) | +### Keyless applications + +The Authentication token, Token validity, and Project linkage checks resolve +the same keyless fallback the rest of the CLI uses (`lib/keyless-target.ts`): +a project with no account session and no linked profile, but a `sk_...` key +on disk (or in `CLERK_SECRET_KEY`/framework env var), is running on an +**unclaimed keyless application** — a legitimate, healthy state, not a broken +one. + +- No token, keyless key present → **pass**, naming the instance and noting + `clerk auth login` claims it. +- Stored session expired, keyless key present → **warn** (not fail): the + keyless key still works, logging in again is optional. +- Signed in (has account credentials) but this directory isn't linked, keyless + key present → **warn**: the account could reach the fuller configuration by + running `clerk link`, so that's called out unlike the fully unclaimed case. +- No token **and** no keyless key found anywhere → still **fail**. Keyless + only changes the outcome when there's actually a secret key to fall back to. + +The Linked application and Instances checks are account-only (the Platform +API application/instance-list concepts have no keyless equivalent), so they +continue to skip for a keyless project — the skip reason names the keyless +application instead of reading like a problem. + ## Auto-Fix (`--fix`) When `--fix` is passed in human mode, the command prompts to fix each @@ -89,7 +113,8 @@ Exit code 1 signals one or more checks failed. ## API Endpoints -| Method | Endpoint | Description | -| ------ | ----------------------------------- | ----------------------------------------------- | -| `GET` | `/oauth/userinfo` | Validates the stored auth token | -| `GET` | `/v1/platform/applications/{appId}` | Verifies the linked app and its instances exist | +| Method | Endpoint | Description | +| ------ | ----------------------------------- | --------------------------------------------------------------- | +| `GET` | `/oauth/userinfo` | Validates the stored auth token | +| `GET` | `/v1/platform/applications/{appId}` | Verifies the linked app and its instances exist | +| `GET` | `/v1/instance` | Names the keyless application (best-effort, via its secret key) | diff --git a/packages/cli-core/src/commands/doctor/checks.ts b/packages/cli-core/src/commands/doctor/checks.ts index b8bf9afad..23bba7a8b 100644 --- a/packages/cli-core/src/commands/doctor/checks.ts +++ b/packages/cli-core/src/commands/doctor/checks.ts @@ -5,6 +5,9 @@ import { fetchUserInfo } from "../../lib/token-exchange.ts"; import { errorMessage, isAuthError, PlapiError } from "../../lib/errors.ts"; import { detectPublishableKeyName, detectSecretKeyName } from "../../lib/framework.ts"; import { parseEnvFile } from "../../lib/dotenv.ts"; +import { hasAccountCredentials } from "../../lib/credential-store.ts"; +import type { KeylessTarget } from "../../lib/keyless-target.ts"; +import { readKeylessBreadcrumb } from "../../lib/keyless.ts"; import { getCurrentVersion, getUpdateChannel, @@ -17,7 +20,7 @@ import { } from "../../lib/update-check.ts"; import { formatHostStateProbeFailures, getAgentHostStateProbe } from "../../lib/host-execution.ts"; import { isAgent } from "../../mode.ts"; -import type { CheckResult, DoctorContext, FixAction } from "./types.ts"; +import type { CheckResult, DoctorContext, FixAction, KeylessInstanceInfo } from "./types.ts"; interface CheckOptions { remedy?: string; @@ -66,15 +69,48 @@ function defineCheck(name: string, fixFactory?: () => FixAction): CheckBuilder { }; } +/** How to refer to an unclaimed keyless application in check output. */ +function keylessLabel(keyless: KeylessTarget, instance: KeylessInstanceInfo | null): string { + const name = instance?.id ? `\`${instance.id}\`` : "this application"; + const env = instance?.environmentType ? ` (${instance.environmentType})` : ""; + return `${name}${env} — secret key from \`${keyless.source}\``; +} + +/** + * How this particular application can be claimed, which is not one answer. + * `clerk auth login` only claims silently when `clerk init` left a claim token + * behind; a key that arrived any other way (an SDK's own keyless.json, a + * hand-copied secret) has no token to redeem and has to be claimed from the + * dashboard instead. + * + * This rides in the check's message rather than its detail: `detail` only + * renders under `--verbose`, and guidance nobody sees by default isn't + * guidance. + */ +async function claimHint(): Promise { + return (await readKeylessBreadcrumb(process.cwd())) + ? "Run `clerk auth login` to claim it." + : "Claim it from the Clerk Dashboard — `clerk auth login` only claims applications `clerk init` created."; +} + export async function checkLoggedIn(ctx: DoctorContext): Promise { const check = defineCheck("Logged in", ctx.fixes.login); const token = await ctx.getToken(); - if (!token) { - return check.fail("Not logged in", { - remedy: "Run `clerk auth login` to authenticate.", - }); + if (token) return check.pass("Logged in (token found in credential store)"); + + // No account session doesn't mean the project is broken: an unclaimed + // keyless application is a legitimate, healthy way to run the CLI. + const keyless = await ctx.getKeylessTarget(); + if (keyless) { + const instance = await ctx.getKeylessInstance(); + return check.pass( + `Not logged in — running on the unclaimed keyless application ${keylessLabel(keyless, instance)}. ${await claimHint()}`, + ); } - return check.pass("Logged in (token found in credential store)"); + + return check.fail("Not logged in", { + remedy: "Run `clerk auth login` to authenticate.", + }); } export async function checkHostExecution(): Promise { @@ -99,7 +135,12 @@ export async function checkHostExecution(): Promise { export async function checkTokenValid(ctx: DoctorContext): Promise { const check = defineCheck("Authentication valid", ctx.fixes.login); const storedToken = await ctx.getToken(); - if (!storedToken) return check.skip("no token"); + if (!storedToken) { + const keyless = await ctx.getKeylessTarget(); + return keyless + ? check.pass("No account session — not required for this keyless application") + : check.skip("no token"); + } try { const token = await ctx.getValidToken(); @@ -108,6 +149,21 @@ export async function checkTokenValid(ctx: DoctorContext): Promise return check.pass(`Authenticated as ${userInfo.email}`); } catch (error) { if (isAuthError(error)) { + // Same fallback whoami uses: an expired session doesn't strand a keyless + // project, so don't tell the user their setup is broken. + const keyless = await ctx.getKeylessTarget(); + if (keyless) { + const instance = await ctx.getKeylessInstance(); + return check.warn( + `Stored session is expired — falling back to the keyless application ${keylessLabel(keyless, instance)}`, + { + remedy: + "Run `clerk auth login` to re-authenticate your account (optional for keyless work).", + fixable: false, + }, + ); + } + return check.fail("Token is expired or invalid", { remedy: "Run `clerk auth login` to re-authenticate.", }); @@ -126,29 +182,61 @@ export async function checkTokenValid(ctx: DoctorContext): Promise export async function checkProjectLinked(ctx: DoctorContext): Promise { const check = defineCheck("Project linked", ctx.fixes.link); const resolved = await ctx.getProfile(); - if (!resolved) { - return check.fail("Not linked to a Clerk application", { - remedy: "Run `clerk link` to associate this project with a Clerk app.", - }); + if (resolved) { + const RESOLUTION_LABELS: Record = { + remote: "git remote", + "git-common-dir": "git repo", + directory: "directory", + }; + const via = `via ${RESOLUTION_LABELS[resolved.resolvedVia] ?? resolved.resolvedVia} (${resolved.path})`; + + return check.pass( + `Linked ${via}`, + `Workspace: ${resolved.profile.workspaceId || "(none)"}\nDev instance: ${resolved.profile.instances.development}\nProd instance: ${resolved.profile.instances.production ?? "(not set)"}`, + ); } - const RESOLUTION_LABELS: Record = { - remote: "git remote", - "git-common-dir": "git repo", - directory: "directory", - }; - const via = `via ${RESOLUTION_LABELS[resolved.resolvedVia] ?? resolved.resolvedVia} (${resolved.path})`; + // Unlinked isn't automatically broken: a project running on an unclaimed + // keyless application has nothing to link yet. + const keyless = await ctx.getKeylessTarget(); + if (keyless) { + const instance = await ctx.getKeylessInstance(); + const label = keylessLabel(keyless, instance); + + // Someone with an account who hasn't linked this directory *could* reach + // the full account configuration — say so, unlike the fully unclaimed case. + if (await hasAccountCredentials()) { + return check.warn( + `Not linked — using the keyless application ${label}, which covers fewer settings`, + { + remedy: "Run `clerk link` to use the full account configuration.", + fixable: true, + }, + ); + } - return check.pass( - `Linked ${via}`, - `Workspace: ${resolved.profile.workspaceId || "(none)"}\nDev instance: ${resolved.profile.instances.development}\nProd instance: ${resolved.profile.instances.production ?? "(not set)"}`, - ); + return check.pass( + `Not linked — running on the unclaimed keyless application ${label}. ${await claimHint()}`, + ); + } + + return check.fail("Not linked to a Clerk application", { + remedy: "Run `clerk link` to associate this project with a Clerk app.", + }); } export async function checkLinkedAppExists(ctx: DoctorContext): Promise { const check = defineCheck("Application reachable", ctx.fixes.link); const token = await ctx.getToken(); - if (!token) return check.skip("not authenticated"); + if (!token) { + // This check is account-only — the Platform API application record has no + // keyless equivalent — so an unclaimed keyless project has nothing to skip + // *over*, just nothing to verify. + const keyless = await ctx.getKeylessTarget(); + return check.skip( + keyless ? "keyless application, no linked app to verify" : "not authenticated", + ); + } const resolved = await ctx.getProfile(); if (!resolved) return check.skip("no project linked"); @@ -175,7 +263,14 @@ export async function checkLinkedAppExists(ctx: DoctorContext): Promise { const check = defineCheck("Instance IDs", ctx.fixes.link); const token = await ctx.getToken(); - if (!token) return check.skip("not authenticated"); + if (!token) { + // A linked profile's dev/prod instance IDs are an account-only concept — + // the secret key on disk already addresses its one instance directly. + const keyless = await ctx.getKeylessTarget(); + return check.skip( + keyless ? "keyless application, no linked instances to verify" : "not authenticated", + ); + } const resolved = await ctx.getProfile(); if (!resolved) return check.skip("no project linked"); diff --git a/packages/cli-core/src/commands/doctor/context.test.ts b/packages/cli-core/src/commands/doctor/context.test.ts index d0e852eb8..fd81fa4dc 100644 --- a/packages/cli-core/src/commands/doctor/context.test.ts +++ b/packages/cli-core/src/commands/doctor/context.test.ts @@ -20,6 +20,16 @@ afterAll(() => resolveProfileSpy.mockRestore()); mock.module("../../lib/git.ts", () => gitStubs); +const mockResolveKeylessTarget = mock(); +mock.module("../../lib/keyless-target.ts", () => ({ + resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), +})); + +const mockBapiRequest = mock(); +mock.module("../../lib/bapi.ts", () => ({ + bapiRequest: (...args: unknown[]) => mockBapiRequest(...args), +})); + // stubFetch instead of mock.module for plapi — mock.module leaks globally in Bun let mockAppResponse: Application | null = null; let mockAppError: Error | null = null; @@ -48,6 +58,10 @@ describe("createDoctorContext", () => { stubFetch((...args: unknown[]) => mockFetch(...args)); process.env.CLERK_PLATFORM_API_KEY = "test_key"; + + mockResolveKeylessTarget.mockReset(); + mockResolveKeylessTarget.mockResolvedValue(undefined); + mockBapiRequest.mockReset(); }); afterEach(() => { @@ -56,6 +70,8 @@ describe("createDoctorContext", () => { mockGetToken.mockReset(); mockResolveProfile.mockReset(); mockFetch.mockReset(); + mockResolveKeylessTarget.mockReset(); + mockBapiRequest.mockReset(); }); describe("getToken", () => { @@ -149,6 +165,77 @@ describe("createDoctorContext", () => { }); }); + describe("getKeylessTarget", () => { + test("returns the same promise on repeated calls", async () => { + const target = { secretKey: "sk_test_keyless", source: ".env.local" }; + mockResolveKeylessTarget.mockResolvedValue(target); + + const ctx = createDoctorContext(); + const p1 = ctx.getKeylessTarget(); + const p2 = ctx.getKeylessTarget(); + + expect(p1).toBe(p2); + expect(await p1).toEqual(target); + expect(mockResolveKeylessTarget).toHaveBeenCalledTimes(1); + }); + + test("returns undefined when no keyless target resolves", async () => { + mockResolveKeylessTarget.mockResolvedValue(undefined); + + const ctx = createDoctorContext(); + expect(await ctx.getKeylessTarget()).toBeUndefined(); + }); + }); + + describe("getKeylessInstance", () => { + test("returns null without hitting BAPI when there is no keyless target", async () => { + mockResolveKeylessTarget.mockResolvedValue(undefined); + + const ctx = createDoctorContext(); + const result = await ctx.getKeylessInstance(); + + expect(result).toBeNull(); + expect(mockBapiRequest).not.toHaveBeenCalled(); + }); + + test("fetches instance info via the keyless secret key, only once", async () => { + mockResolveKeylessTarget.mockResolvedValue({ + secretKey: "sk_test_keyless", + source: ".env.local", + }); + mockBapiRequest.mockResolvedValue({ + status: 200, + body: { id: "ins_keyless_1", environment_type: "development" }, + }); + + const ctx = createDoctorContext(); + const p1 = ctx.getKeylessInstance(); + const p2 = ctx.getKeylessInstance(); + + expect(p1).toBe(p2); + expect(await p1).toEqual({ id: "ins_keyless_1", environmentType: "development" }); + expect(mockBapiRequest).toHaveBeenCalledTimes(1); + expect(mockBapiRequest).toHaveBeenCalledWith( + expect.objectContaining({ + method: "GET", + path: "/v1/instance", + secretKey: "sk_test_keyless", + }), + ); + }); + + test("returns null (not a throw) when the instance fetch fails", async () => { + mockResolveKeylessTarget.mockResolvedValue({ + secretKey: "sk_test_keyless", + source: ".env.local", + }); + mockBapiRequest.mockRejectedValue(new Error("network down")); + + const ctx = createDoctorContext(); + expect(await ctx.getKeylessInstance()).toBeNull(); + }); + }); + describe("fixes", () => { test("fix factories return FixAction objects with labels", () => { const ctx = createDoctorContext(); diff --git a/packages/cli-core/src/commands/doctor/context.ts b/packages/cli-core/src/commands/doctor/context.ts index 9bcde632d..ab3fb3bd1 100644 --- a/packages/cli-core/src/commands/doctor/context.ts +++ b/packages/cli-core/src/commands/doctor/context.ts @@ -1,13 +1,19 @@ import { getToken, getValidToken } from "../../lib/credential-store.ts"; import { resolveProfile } from "../../lib/config.ts"; import { fetchApplication, type Application } from "../../lib/plapi.ts"; -import type { DoctorContext, ResolvedProfile } from "./types.ts"; +import { resolveKeylessTarget, type KeylessTarget } from "../../lib/keyless-target.ts"; +import { bapiRequest } from "../../lib/bapi.ts"; +import { log } from "../../lib/log.ts"; +import { errorMessage } from "../../lib/errors.ts"; +import type { DoctorContext, KeylessInstanceInfo, ResolvedProfile } from "./types.ts"; export function createDoctorContext(): DoctorContext { let tokenPromise: Promise | undefined; let validTokenPromise: Promise | undefined; let profilePromise: Promise | undefined; let appPromise: Promise | undefined; + let keylessPromise: Promise | undefined; + let keylessInstancePromise: Promise | undefined; const ctx: DoctorContext = { getToken() { @@ -44,6 +50,40 @@ export function createDoctorContext(): DoctorContext { return appPromise; }, + getKeylessTarget() { + if (!keylessPromise) { + // Same resolution every other command uses: an explicit --app/link rules + // keyless out, but account credentials alone don't (see keyless-target.ts). + keylessPromise = resolveKeylessTarget({ cwd: process.cwd() }); + } + return keylessPromise; + }, + + getKeylessInstance() { + if (!keylessInstancePromise) { + keylessInstancePromise = (async () => { + const keyless = await ctx.getKeylessTarget(); + if (!keyless) return null; + + try { + const response = await bapiRequest({ + method: "GET", + path: "/v1/instance", + secretKey: keyless.secretKey, + }); + const body = response.body as { id?: string; environment_type?: string }; + return { id: body.id ?? null, environmentType: body.environment_type ?? null }; + } catch (error) { + // Naming the instance is a nice-to-have here — the checks that + // actually need the target already have it via getKeylessTarget(). + log.debug(`doctor: could not fetch keyless instance info (${errorMessage(error)})`); + return null; + } + })(); + } + return keylessInstancePromise; + }, + fixes: { login: () => ({ label: "Log in with clerk auth login", diff --git a/packages/cli-core/src/commands/doctor/doctor.test.ts b/packages/cli-core/src/commands/doctor/doctor.test.ts index cfc174668..a6e110170 100644 --- a/packages/cli-core/src/commands/doctor/doctor.test.ts +++ b/packages/cli-core/src/commands/doctor/doctor.test.ts @@ -4,9 +4,21 @@ import { join } from "node:path"; import { tmpdir } from "node:os"; import { ApiError, AuthError } from "../../lib/errors.ts"; import { _setConfigDir } from "../../lib/config.ts"; -import { gitStubs, tokenExchangeStubs, stubFetch } from "../../test/lib/stubs.ts"; -import type { CheckResult, CheckStatus, DoctorContext, ResolvedProfile } from "./types.ts"; +import { + credentialStoreStubs, + gitStubs, + tokenExchangeStubs, + stubFetch, +} from "../../test/lib/stubs.ts"; +import type { + CheckResult, + CheckStatus, + DoctorContext, + KeylessInstanceInfo, + ResolvedProfile, +} from "./types.ts"; import type { Application } from "../../lib/plapi.ts"; +import type { KeylessTarget } from "../../lib/keyless-target.ts"; let mockUserInfo: { userId: string; email: string } | null = null; let mockUserInfoError: Error | null = null; @@ -23,6 +35,7 @@ mock.module("../../lib/token-exchange.ts", () => ({ }, })); +mock.module("../../lib/credential-store.ts", () => credentialStoreStubs); mock.module("../../lib/git.ts", () => gitStubs); mock.module("../../lib/host-execution.ts", () => ({ getAgentHostStateProbe: async () => mockHostStateProbe, @@ -92,6 +105,8 @@ function createMockContext( }; application?: Application | null; applicationError?: Error; + keylessTarget?: KeylessTarget; + keylessInstance?: KeylessInstanceInfo | null; } = {}, ): DoctorContext { return { @@ -105,6 +120,8 @@ function createMockContext( if (overrides.applicationError) throw overrides.applicationError; return overrides.application ?? null; }, + getKeylessTarget: async () => overrides.keylessTarget, + getKeylessInstance: async () => overrides.keylessInstance ?? null, fixes: { login: noopFix, link: noopFix, @@ -113,6 +130,16 @@ function createMockContext( }; } +const mockKeylessTarget: KeylessTarget = { + secretKey: "sk_test_keyless", + source: ".env.local", +}; + +const mockKeylessInstance: KeylessInstanceInfo = { + id: "ins_keyless_1", + environmentType: "development", +}; + interface ExpectedCheck { name: string; status: CheckStatus; @@ -191,6 +218,22 @@ describe("checkLoggedIn", () => { fix: true, }); }); + + test("pass (not fail) when no token but an unclaimed keyless application is present", async () => { + const ctx = createMockContext({ + token: null, + keylessTarget: mockKeylessTarget, + keylessInstance: mockKeylessInstance, + }); + const result = await checkLoggedIn(ctx); + expectCheck(result, { + name: "Logged in", + status: "pass", + // The claim hint has to ride in the message: `detail` only renders + // under --verbose, and guidance nobody sees by default isn't guidance. + message: ["unclaimed keyless application", "ins_keyless_1", "Claim it"], + }); + }); }); describe("checkHostExecution", () => { @@ -299,6 +342,29 @@ describe("checkTokenValid", () => { const result = await checkTokenValid(ctx); expectCheck(result, { name: "Authentication valid", status: "warn", message: "Skipped" }); }); + + test("pass (not skip) when no token but an unclaimed keyless application is present", async () => { + const ctx = createMockContext({ token: null, keylessTarget: mockKeylessTarget }); + const result = await checkTokenValid(ctx); + expectCheck(result, { + name: "Authentication valid", + status: "pass", + message: "No account session", + }); + }); + + test("warn (not fail) when token is expired but a keyless application is present", async () => { + mockUserInfoError = new ApiError(401, "Unauthorized"); + const ctx = createMockContext({ token: "expired_token", keylessTarget: mockKeylessTarget }); + const result = await checkTokenValid(ctx); + expectCheck(result, { + name: "Authentication valid", + status: "warn", + message: ["Stored session is expired", "keyless application"], + remedy: "clerk auth login", + fix: false, + }); + }); }); describe("checkProjectLinked", () => { @@ -324,6 +390,39 @@ describe("checkProjectLinked", () => { fix: true, }); }); + + test("pass (not fail) when unlinked but running an unclaimed keyless application", async () => { + delete process.env.CLERK_PLATFORM_API_KEY; // no account credentials at all + const ctx = createMockContext({ + keylessTarget: mockKeylessTarget, + keylessInstance: mockKeylessInstance, + }); + const result = await checkProjectLinked(ctx); + expectCheck(result, { + name: "Project linked", + status: "pass", + // The claim hint has to ride in the message: `detail` only renders + // under --verbose, and guidance nobody sees by default isn't guidance. + message: ["unclaimed keyless application", "ins_keyless_1", "Claim it"], + }); + }); + + test("warn (not fail) when signed in but unlinked, falling back to a keyless application", async () => { + // beforeEach sets CLERK_PLATFORM_API_KEY, so hasAccountCredentials() is true here — + // the directory *could* reach the full account configuration by linking. + const ctx = createMockContext({ + keylessTarget: mockKeylessTarget, + keylessInstance: mockKeylessInstance, + }); + const result = await checkProjectLinked(ctx); + expectCheck(result, { + name: "Project linked", + status: "warn", + message: ["keyless application", "fewer settings"], + remedy: "clerk link", + fix: true, + }); + }); }); describe("checkLinkedAppExists", () => { @@ -384,6 +483,16 @@ describe("checkLinkedAppExists", () => { const result = await checkLinkedAppExists(ctx); expectCheck(result, { name: "Application reachable", status: "warn", message: "Skipped" }); }); + + test("skip reason names the keyless application instead of implying a problem", async () => { + const ctx = createMockContext({ token: null, keylessTarget: mockKeylessTarget }); + const result = await checkLinkedAppExists(ctx); + expectCheck(result, { + name: "Application reachable", + status: "warn", + message: ["Skipped", "keyless application"], + }); + }); }); describe("checkInstances", () => { @@ -453,6 +562,16 @@ describe("checkInstances", () => { const result = await checkInstances(ctx); expectCheck(result, { name: "Instance IDs", status: "warn", message: "Skipped" }); }); + + test("skip reason names the keyless application instead of implying a problem", async () => { + const ctx = createMockContext({ token: null, keylessTarget: mockKeylessTarget }); + const result = await checkInstances(ctx); + expectCheck(result, { + name: "Instance IDs", + status: "warn", + message: ["Skipped", "keyless application"], + }); + }); }); describe("checkEnvVars", () => { diff --git a/packages/cli-core/src/commands/doctor/types.ts b/packages/cli-core/src/commands/doctor/types.ts index dfe87efaf..68bfbcf90 100644 --- a/packages/cli-core/src/commands/doctor/types.ts +++ b/packages/cli-core/src/commands/doctor/types.ts @@ -1,5 +1,6 @@ import type { resolveProfile } from "../../lib/config.ts"; import type { Application } from "../../lib/plapi.ts"; +import type { KeylessTarget } from "../../lib/keyless-target.ts"; export type CheckStatus = "pass" | "warn" | "fail"; @@ -19,11 +20,25 @@ export interface CheckResult { fix?: FixAction; } +/** The identity of an unclaimed keyless application, fetched via its own secret key. */ +export interface KeylessInstanceInfo { + id: string | null; + environmentType: string | null; +} + export interface DoctorContext { getToken(): Promise; getValidToken(): Promise; getProfile(): Promise; getApplication(): Promise; + /** + * Resolves the same keyless fallback the rest of the CLI uses (see + * `lib/keyless-target.ts`), so doctor treats an unclaimed keyless project as + * the legitimate state it is instead of failing the auth/link checks. + */ + getKeylessTarget(): Promise; + /** Best-effort identity of the keyless instance, for naming it in check output. */ + getKeylessInstance(): Promise; fixes: { login: () => FixAction; link: () => FixAction; diff --git a/packages/cli-core/src/commands/env/README.md b/packages/cli-core/src/commands/env/README.md index 77e28f02a..a0b0bbb5b 100644 --- a/packages/cli-core/src/commands/env/README.md +++ b/packages/cli-core/src/commands/env/README.md @@ -2,7 +2,9 @@ Pulls Clerk API keys for the linked instance and merges them into the project's `.env` file. -For an unclaimed **keyless** application there is no account to pull from — its keys only exist on this machine. When the directory isn't linked and no `--app` is passed, `env pull` instead copies the keys it finds locally (env var, `.env`/`.env.local`, or the `.clerk/.tmp/keyless.json` an SDK wrote for itself) into the env file the framework reads, and makes no API call. This is what materializes an SDK-created keyless app into `.env.local`. If only the secret key can be found, it's written and a warning names the missing publishable key. Resolution order lives in [`lib/keyless-target.ts`](../../lib/keyless-target.ts). +For an unclaimed **keyless** application there is no account to pull from — its keys only exist on this machine. When the directory isn't linked and no `--app` is passed, `env pull` instead copies the keys it finds locally (env var, `.env`/`.env.local`, or the `.clerk/.tmp/keyless.json` an SDK wrote for itself) into the env file the framework reads. This is what materializes an SDK-created keyless app into `.env.local`. If only the secret key can be found, it's written and a warning names the missing publishable key. Resolution order lives in [`lib/keyless-target.ts`](../../lib/keyless-target.ts). + +The secret key and publishable key are found independently and can each belong to a _different_ application (e.g. leftovers from two keyless apps in the same `.env.local`). Before writing, `env pull` calls `GET /v1/domains` with the secret key and confirms the publishable key's Frontend API host (decoded via `decodePublishableKey` in `lib/fapi.ts`) matches one of that instance's own domains. A mismatch aborts the pull with an error and writes nothing — a wrong pair on disk produces an app that fails at runtime in a way that's very hard to trace, so this is stricter than `clerk whoami`, which only warns about the same mismatch (see [`commands/whoami/README.md`](../whoami/README.md)). ## Usage @@ -69,10 +71,11 @@ sequenceDiagram ## API Endpoints -| Step | Method | Endpoint | Notes | -| ----------------- | ------ | ----------------------------------- | ----------------------------------------------------------------------- | -| Auth | — | Local config | Uses `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or human-mode prompt | -| Fetch application | `GET` | `/v1/platform/applications/{appId}` | Returns all instances with keys | +| Step | Method | Endpoint | Notes | +| ------------------------------------------ | ------ | ----------------------------------- | ------------------------------------------------------------------------------ | +| Auth | — | Local config | Uses `CLERK_PLATFORM_API_KEY`, `clerk auth login`, or human-mode prompt | +| Fetch application | `GET` | `/v1/platform/applications/{appId}` | Returns all instances with keys | +| Verify keyless pairing (keyless path only) | `GET` | `/v1/domains` | Only when a local publishable key was found; authenticated with the secret key | ## Framework Detection diff --git a/packages/cli-core/src/commands/env/pull.test.ts b/packages/cli-core/src/commands/env/pull.test.ts index 9d253c63d..8f69afff1 100644 --- a/packages/cli-core/src/commands/env/pull.test.ts +++ b/packages/cli-core/src/commands/env/pull.test.ts @@ -676,5 +676,65 @@ describe("env pull", () => { const content = await Bun.file(join(tempDir, ".env")).text(); expect(content).toContain("CLERK_PUBLISHABLE_KEY=pk_test_abc123"); expect(content).not.toContain("CLERK_SECRET_KEY"); + describe("keyless", () => { + // Encodes `.clerk.accounts.dev$` the way a real publishable key + // would, so `decodePublishableKey` (used by the pairing check) sees the + // same shape it would in production instead of a fixture-only format. + const encodeFapiHost = (host: string) => + `pk_test_${Buffer.from(`${host}.clerk.accounts.dev$`).toString("base64").replace(/=+$/, "")}`; + const MATCHING_PK = encodeFapiHost("match"); + const MISMATCHED_PK = encodeFapiHost("other"); + + function stubDomains(frontendApiUrl: string): void { + stubFetch(async (input) => { + const url = input.toString(); + if (url.includes("/v1/domains")) { + return new Response(JSON.stringify({ data: [{ frontend_api_url: frontendApiUrl }] }), { + status: 200, + }); + } + throw new Error(`unexpected fetch in keyless test: ${url}`); + }); + } + + beforeEach(() => { + // No linked profile and no --app is what routes env pull to the keyless + // path (see lib/keyless-target.ts); CLERK_PLATFORM_API_KEY would only + // matter for the account path. + delete process.env.CLERK_PLATFORM_API_KEY; + process.env.CLERK_BACKEND_API_URL = "https://test-bapi.clerk.com"; + process.env.CLERK_SECRET_KEY = "sk_test_keyless"; + }); + + test("writes a matching pair", async () => { + process.env.CLERK_PUBLISHABLE_KEY = MATCHING_PK; + stubDomains("https://match.clerk.accounts.dev"); + + await runEnvPull(); + + const content = await Bun.file(join(tempDir, ".env.local")).text(); + expect(content).toContain("CLERK_SECRET_KEY=sk_test_keyless"); + expect(content).toContain(`CLERK_PUBLISHABLE_KEY=${MATCHING_PK}`); + }); + + test("refuses to write a publishable key that addresses a different application", async () => { + process.env.CLERK_PUBLISHABLE_KEY = MISMATCHED_PK; + stubDomains("https://match.clerk.accounts.dev"); + + await expect(runEnvPull()).rejects.toThrow(/doesn't belong to the application/); + + // Nothing should have been written — a partial or wrong pair on disk is + // worse than the command simply refusing. + expect(await Bun.file(join(tempDir, ".env.local")).exists()).toBe(false); + }); + + test("writes the secret key alone, with a warning, when no publishable key is found locally", async () => { + await runEnvPull(); + + const content = await Bun.file(join(tempDir, ".env.local")).text(); + expect(content).toContain("CLERK_SECRET_KEY=sk_test_keyless"); + expect(content).not.toContain("CLERK_PUBLISHABLE_KEY"); + expect(captured.err).toContain("No publishable key found locally"); + }); }); }); diff --git a/packages/cli-core/src/commands/env/pull.ts b/packages/cli-core/src/commands/env/pull.ts index d1d6423bc..50d4aa7d8 100644 --- a/packages/cli-core/src/commands/env/pull.ts +++ b/packages/cli-core/src/commands/env/pull.ts @@ -12,6 +12,7 @@ import { import { CliError, ERROR_CODE, withApiContext } from "../../lib/errors.ts"; import { findLocalPublishableKey, + hasKeyPairMismatch, resolveKeylessTarget, type KeylessTarget, } from "../../lib/keyless-target.ts"; @@ -132,6 +133,24 @@ async function pullKeylessKeys( findLocalPublishableKey(cwd), ]); + // The secret and publishable key were found independently and may not name + // the same application. Writing a mismatched pair is worse than writing + // nothing: it produces an app that fails at runtime in a way that's very + // hard to trace back to its cause, so this is the stricter of the two + // checks — whoami only warns, this refuses to write. + if (publishableKey) { + const mismatch = await withApiContext( + hasKeyPairMismatch(keyless, publishableKey), + `Failed to verify the keyless secret key from \`${keyless.source}\``, + ); + if (mismatch) { + throw new CliError( + `The publishable key found locally doesn't belong to the application the secret key from \`${keyless.source}\` addresses. Writing this pair would leave the server trusting one app while the browser talks to another.\n` + + `Remove the mismatched ${publishableKeyName} from your env files, or run \`clerk auth login\` to claim the intended application, then pull again.`, + ); + } + } + const targetFile = await resolveTargetFile(cwd, fileFlag, preferredEnvFile); const displayPath = fileFlag ?? basename(targetFile); diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index 4e39b9586..c82bbeef0 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -14,6 +14,7 @@ clerk init --starter --framework next --pm bun --name my-app clerk init --starter --framework next --keyless clerk init --login clerk init --template b2b-saas +clerk init --keyless --fresh clerk init -y clerk init --yes clerk init --no-skills @@ -21,18 +22,19 @@ clerk init --no-skills ## Options -| Option | Description | -| ----------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--framework ` | Framework to set up (skips auto-detection). Valid values: `next`, `astro`, `nuxt`, `tanstack-start`, `react-router`, `vue`, `expo`, `react`, `javascript`, `js`, `express`, `fastify`, `ios`, `android` | -| `--pm ` | Package manager to use. Valid values: `bun`, `pnpm`, `yarn`, `npm`. Skips the PM prompt (bootstrap) or overrides lockfile detection (existing project) | -| `--name ` | Project name for `--starter` (skips prompt). Must be lowercase, no spaces, no path separators | -| `--app ` | Application ID to link (skips the interactive app picker during authenticated linking) | -| `--starter` | Bootstrap a new project from a starter template (runs the framework generator, installs deps, and scaffolds Clerk) | -| `--keyless` | Force auto-generated temporary development keys, even when logged in. Only valid on a keyless-capable framework; cannot be combined with `--login` or `--app` | -| `--login` | Force the authenticated flow: log in (interactively if needed) and link a real application instead of keyless keys. Errors in agent mode when unauthenticated (agents can't run OAuth) | -| `--template ` | Pre-configure the keyless application at creation: `b2b-saas`, `b2c-saas`, `native`, `waitlist`. Keyless-only — cannot be combined with `--login` | -| `-y, --yes` | Skip y/n confirmation prompts only. It neither forces nor bypasses keyless — the strategy is picked by auth state, mode, and flags | -| `--no-skills` | Skip the optional agent skills install prompt at the end of init | +| Option | Description | +| ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `--framework ` | Framework to set up (skips auto-detection). Valid values: `next`, `astro`, `nuxt`, `tanstack-start`, `react-router`, `vue`, `expo`, `react`, `javascript`, `js`, `express`, `fastify`, `ios`, `android` | +| `--pm ` | Package manager to use. Valid values: `bun`, `pnpm`, `yarn`, `npm`. Skips the PM prompt (bootstrap) or overrides lockfile detection (existing project) | +| `--name ` | Project name for `--starter` (skips prompt). Must be lowercase, no spaces, no path separators | +| `--app ` | Application ID to link (skips the interactive app picker during authenticated linking) | +| `--starter` | Bootstrap a new project from a starter template (runs the framework generator, installs deps, and scaffolds Clerk) | +| `--keyless` | Force auto-generated temporary development keys, even when logged in. Only valid on a keyless-capable framework; cannot be combined with `--login` or `--app` | +| `--login` | Force the authenticated flow: log in (interactively if needed) and link a real application instead of keyless keys. Errors in agent mode when unauthenticated (agents can't run OAuth) | +| `--template ` | Pre-configure the keyless application at creation: `b2b-saas`, `b2c-saas`, `native`, `waitlist`. Only applies when the run resolves to keyless — errors otherwise (see [Application templates](#application-templates)); cannot be combined with `--login` | +| `--fresh` | Replace an existing unclaimed keyless application with a new one, instead of keeping it (see [Keyless breadcrumb](#keyless-breadcrumb)). Only applies when the run resolves to keyless — errors otherwise; cannot be combined with `--login` | +| `-y, --yes` | Skip y/n confirmation prompts only. It neither forces nor bypasses keyless — the strategy is picked by auth state, mode, and flags. It does **not** replace an existing unclaimed keyless app — that still requires `--fresh` | +| `--no-skills` | Skip the optional agent skills install prompt at the end of init | ## Agent Mode @@ -47,19 +49,22 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful - When **unauthenticated**, init uses keyless: the app runs on auto-generated dev keys, and init writes a `.clerk/keyless.json` breadcrumb so the next `clerk auth login` claims the app automatically. - For frameworks that require API keys, init will not pick or create an app in agent mode; pass `--app ` or link the project first to pull real keys - `--login` while unauthenticated exits with a usage error (agents can't complete the interactive browser login) +- Agent mode never trusts the mere _presence_ of a stored credential the way human mode does — a stored session that turns out to be expired/broken (e.g. keyring holds a stale OAuth session) is validated before init decides it's "authenticated". A broken credential is treated as unauthenticated, which routes a keyless-capable framework to keyless instead of blocking on a browser OAuth round-trip an agent can never complete. If `--login` (or a real app target) forces the authenticated flow anyway and the credential turns out broken, init exits with a usage error instead of attempting an interactive login +- Agent mode never mints a fresh keyless application over an existing unclaimed one on re-run — see [Keyless breadcrumb](#keyless-breadcrumb) ## Flow 1. Gathers project context (framework, router variant, TypeScript, `src/` directory, package manager) -2. Determines the strategy (in precedence order): +2. Determines the strategy (in precedence order). In agent mode, "authenticated" here means a _validated_ credential (a real `CLERK_PLATFORM_API_KEY`, or a stored session that still exchanges for a valid token) — not just the presence of something in the keyring, since agent mode has no interactive fallback if a stale credential turns out to be unusable: - **`--keyless`**: forces keyless mode, even when logged in. Only valid on a keyless-capable framework, and cannot be combined with `--login` or `--app` (usage errors otherwise). The app runs on auto-generated dev keys; init writes a `.clerk/keyless.json` breadcrumb so the next `clerk auth login` claims the app automatically - - **`--login`**: forces the authenticated flow. In agent mode while unauthenticated this exits with a usage error, since agents can't complete the interactive browser login + - **`--login`**: forces the authenticated flow. In agent mode while unauthenticated (or while stored credentials are broken) this exits with a usage error, since agents can't complete the interactive browser login - **Real app target** (`--app` or linked profile): authenticates, links if needed, and pulls real API keys into `.env` - **Agent + non-keyless framework + no real app target**: scaffolds locally and prints manual setup instructions instead of selecting or creating an app - **Agent + keyless-capable framework + authenticated + no real app target**: creates a real Clerk app named after the project, links it, and pulls real API keys into `.env` - - **Agent + keyless-capable framework + unauthenticated + no real app target**: uses keyless mode — the app runs on auto-generated dev keys and the breadcrumb lets the next `clerk auth login` claim it + - **Agent + keyless-capable framework + unauthenticated + no real app target**: uses keyless mode — the app runs on auto-generated dev keys and the breadcrumb lets the next `clerk auth login` claim it. A broken/stale stored credential (present in the keyring but no longer valid) is treated the same as unauthenticated, so this is also the fallback when the presence-only check would have wrongly said "authenticated" - **Human mode + bootstrap + keyless-capable framework + not authenticated**: uses keyless mode - **Human mode + existing project + not authenticated**: runs the authenticated flow, which triggers an interactive login so real keys can be pulled. `-y` does not bypass this — it only suppresses y/n confirmation prompts, not authentication + - `--template` and `--fresh` are rejected with a usage error whenever the resolved strategy above isn't keyless — see [Application templates](#application-templates) and [Keyless breadcrumb](#keyless-breadcrumb) 3. **Authenticated mode only**: authenticates via `clerk auth login` (skipped if already authenticated) and links the project via `clerk link` (skipped if already linked) 4. Displays detected framework and variant 5. Detects existing auth libraries (NextAuth, Auth0, Supabase, Firebase, Passport, Better Auth, Kinde) and shows migration guidance @@ -72,7 +77,7 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful 12. Scans for issues: hardcoded keys, leftover auth-library imports, stale API calls 13. Prints a summary of created, modified, and skipped files with recommendations 14. **Authenticated mode**: pulls development instance API keys via `clerk env pull` -15. **Unauthenticated mode**: prints instructions for development without API keys and how to connect a Clerk account later +15. **Unauthenticated mode**: mints a keyless application and prints instructions for development without API keys and how to connect a Clerk account later — unless an unclaimed keyless app already exists for this project (see [Re-running init on an already-keyless project](#re-running-init-on-an-already-keyless-project)), in which case the existing keys are kept and reported instead 16. Optionally installs Clerk agent skills (cli + core + features, plus a framework-specific skill) via the project's package runner (see [Agent skills install](#agent-skills-install)) ## Framework Detection @@ -284,7 +289,7 @@ See [auth/README.md](../auth/README.md), [link/README.md](../link/README.md), an | `native` | Native/mobile application | | `waitlist` | Waitlist sign-up mode | -The template only applies when an application is created, so it is keyless-only: combining it with `--login` exits with a usage error. Settings can still be changed afterwards with `clerk config patch`, which also works without an account (see [config keyless mode](../config/README.md#keyless-mode)). +The template only applies when a _new_ application is actually created, so `--template` is rejected with a usage error whenever the resolved strategy isn't keyless — whether that's because of an explicit conflicting flag (`--login`, or `--app` once the strategy resolves) or because the run is simply already authenticated (e.g. `CLERK_PLATFORM_API_KEY` is set) or the framework doesn't support keyless at all. The error names the reason, so `--template` is never silently dropped: add `--keyless` to force a keyless app, or drop `--template`. Settings can still be changed afterwards with `clerk config patch`, which also works without an account (see [config keyless mode](../config/README.md#keyless-mode)). ## Keyless breadcrumb @@ -298,3 +303,13 @@ In keyless mode, after calling `POST /v1/accountless_applications`, `clerk init` ``` `.clerk/` is automatically added to `.gitignore` when the breadcrumb is written. The breadcrumb is removed after a successful claim (or when the claim token expires/is already consumed). + +### Re-running init on an already-keyless project + +The breadcrumb is also what protects an unclaimed keyless app from being orphaned by a later `clerk init` run. As long as `.clerk/keyless.json` is present, the application it points at hasn't been claimed yet — its claim token, and anything configured on or created in that app, only exist as long as the breadcrumb and the env keys pointing at it survive. So whenever init resolves to keyless mode and finds an existing breadcrumb, it does **not** silently mint a replacement application and overwrite the env keys and breadcrumb with the new one's: + +- **Human mode** (no `-y`): prompts `This project already has an unclaimed keyless application (created ). Replace it with a new one?`, defaulting to **no**. Declining keeps the existing keys and breadcrumb untouched. +- **Human mode with `-y`, and all agent-mode runs**: never prompt, and default to the same safe answer — **keep the existing application**. `-y` and agent mode both mean "skip confirmations", not "consent to destroying an app that might already have configuration or users on it". +- **`--fresh`**: the explicit escape hatch. Skips the check entirely and mints a new application (and overwrites the env keys and breadcrumb), even in agent mode or with `-y`. Like `--template`, it's a usage error when combined with `--login` or whenever the run doesn't resolve to keyless. + +If no breadcrumb exists (first run, or the previous app was already claimed and the breadcrumb removed), init proceeds exactly as before — there's nothing to protect. diff --git a/packages/cli-core/src/commands/init/heuristics.ts b/packages/cli-core/src/commands/init/heuristics.ts index 615568b36..6b22e10c2 100644 --- a/packages/cli-core/src/commands/init/heuristics.ts +++ b/packages/cli-core/src/commands/init/heuristics.ts @@ -157,3 +157,15 @@ export function printKeylessInfo(envFile: string): void { ]; log.info(lines.map(dim).join("\n")); } + +/** + * Printed instead of `printKeylessInfo` when init keeps an existing unclaimed + * keyless app rather than minting a replacement (see `shouldKeepExistingKeyless`). + */ +export function printExistingKeylessInfo(envFile: string): void { + const lines = [ + `\n This project already has an unclaimed keyless application (keys in ${envFile}).`, + ` Run ${bold("clerk auth login")} to claim it, or ${bold("clerk init --keyless --fresh")} to replace it with a new one.\n`, + ]; + log.info(lines.map(dim).join("\n")); +} diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index e2b46c920..44c7d79be 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -15,11 +15,13 @@ import { import { resolveProfile } from "../../lib/config.js"; import { deriveProjectName } from "../../lib/project-name.js"; import { log } from "../../lib/log.js"; +import { confirm } from "../../lib/prompts.ts"; import { createAccountlessApp, writeKeysToEnvFile, parseClaimToken, writeKeylessBreadcrumb, + readKeylessBreadcrumb, KEYLESS_TEMPLATES, type KeylessTemplate, } from "../../lib/keyless.js"; @@ -36,6 +38,7 @@ import { checkGitDirty, printOutro, printKeylessInfo, + printExistingKeylessInfo, getAuthenticatedEmail, isAuthenticated, } from "./heuristics.js"; @@ -68,6 +71,8 @@ type InitOptions = { login?: boolean; /** Pre-configure the keyless application from a Clerk application template. */ template?: KeylessTemplate; + /** Replace an existing unclaimed keyless application instead of keeping it. */ + fresh?: boolean; }; export async function init(options: InitOptions = {}) { @@ -106,7 +111,17 @@ export async function init(options: InitOptions = {}) { const optsKeyless = options.keyless === true; // Skip auth-related I/O entirely when the user opted into keyless — those // values are not consumed once the strategy resolves to "keyless". - const authed = optsKeyless ? false : await isAuthenticated(); + // + // Agent mode has no way to recover if this lies: a human who turns out to be + // unauthenticated just gets prompted to log in, but an agent that trusts a + // stale/broken credential ends up blocked on an interactive browser OAuth + // round-trip it can never complete. So agent mode validates the credential + // (it can fall back to keyless) instead of trusting mere presence. + const authed = optsKeyless + ? false + : agent + ? await isAuthenticatedForAgent() + : await isAuthenticated(); const linkedProfile = !optsKeyless && agent && !options.app ? await resolveProfile(ctx.cwd) : undefined; const hasRealAppTarget = Boolean(options.app || linkedProfile); @@ -121,6 +136,8 @@ export async function init(options: InitOptions = {}) { framework: ctx.framework, }); + assertKeylessOnlyFlags(options, strategy); + if (strategy === "authenticate") { bar(); const createIfMissing = agent @@ -146,7 +163,11 @@ export async function init(options: InitOptions = {}) { } bar(); - await runStrategy(strategy, ctx, options.template); + await runStrategy(strategy, ctx, { + template: options.template, + fresh: options.fresh === true, + skipConfirm: overrides.skipConfirm, + }); // Native platforms (iOS/Android) have no npx/Node toolchain to run `skills add` with. if (options.skills !== false && isNpmFramework(ctx.framework)) { @@ -165,8 +186,9 @@ export async function init(options: InitOptions = {}) { /** * Rejects flag combinations that can't both be honoured, before anything is - * bootstrapped on disk. `--keyless` and `--template` describe an application the - * CLI creates; `--login` and `--app` describe one that already exists. + * bootstrapped on disk. `--keyless`, `--template`, and `--fresh` describe an + * application the CLI creates; `--login` and `--app` describe one that + * already exists. */ async function assertUsableFlags(options: InitOptions, agent: boolean): Promise { if (options.keyless && options.login) { @@ -182,13 +204,63 @@ async function assertUsableFlags(options: InitOptions, agent: boolean): Promise< "--template applies to keyless applications and cannot be combined with --login.", ); } - if (options.login && agent && !(await isAuthenticated())) { + if (options.fresh && options.login) { + throwUsageError("--fresh applies to keyless applications and cannot be combined with --login."); + } + // Presence-only here would repeat the hang below: an agent can't complete an + // interactive login, so a stored-but-broken credential must read as + // unauthenticated rather than let this guard wave the request through. + if (options.login && agent && !(await isAuthenticatedForAgent())) { throwUsageError( "--login requires an interactive terminal to complete the browser login. Ask the user to run `clerk auth login`, then re-run `clerk init`.", ); } } +/** + * Agent-mode variant of `isAuthenticated()`. The human-mode presence check + * (see `heuristics.isAuthenticated`) deliberately doesn't validate the + * credential, because a human who turns out to be unauthenticated just gets + * an interactive login prompt. An agent has no such fallback — if it trusts a + * stale/broken credential, it ends up blocked on a browser OAuth round-trip + * that can never complete. So this validates before trusting: a real API key + * is accepted outright (no OAuth involved), everything else must actually + * resolve to a user. + */ +async function isAuthenticatedForAgent(): Promise { + if (process.env.CLERK_PLATFORM_API_KEY) return true; + return (await getAuthenticatedEmail()) !== null; +} + +/** + * `--template` and `--fresh` only take effect when init creates a keyless + * application. Silently dropping them when the strategy resolves elsewhere + * (the pre-fix behaviour for `--template`) leaves the user believing they got + * a shaped or replaced app when they didn't — so fail loudly instead, the + * same way `--keyless`+`--app` does above. This runs after strategy + * resolution because that's the earliest point the real strategy — not just + * the flags that might influence it — is known. + */ +function assertKeylessOnlyFlags(options: InitOptions, strategy: InitStrategy): void { + if (strategy === "keyless") return; + + const reason = + strategy === "manual" + ? "this framework does not support keyless mode" + : "this run resolved to the authenticated flow instead (already signed in, --app was set, or a project is already linked)"; + + if (options.template) { + throwUsageError( + `--template only applies to keyless applications, but ${reason}. Add --keyless to force a keyless app, or drop --template.`, + ); + } + if (options.fresh) { + throwUsageError( + `--fresh only applies to keyless applications, but ${reason}. Add --keyless to force a keyless app, or drop --fresh.`, + ); + } +} + type ResolvedContext = { ctx: ProjectContext; bootstrap: BootstrapResult | null; @@ -328,10 +400,18 @@ function pickStrategy({ return "authenticate"; } +type KeylessRunOptions = { + template?: KeylessTemplate; + /** Escape hatch for "give me a fresh one": mint a new app even if an unclaimed one already exists. */ + fresh: boolean; + /** Agent mode and `-y` both skip y/n prompts, so both must default to *not* replacing. */ + skipConfirm: boolean; +}; + async function runStrategy( strategy: InitStrategy, ctx: ProjectContext, - template?: KeylessTemplate, + keylessOptions: KeylessRunOptions, ): Promise { switch (strategy) { case "manual": @@ -341,7 +421,7 @@ async function runStrategy( await pull({ file: ctx.envFile, cwd: ctx.cwd }); return; case "keyless": - await setupKeylessApp(ctx.cwd, ctx.framework.dep, ctx.envFile, template); + await setupKeylessApp(ctx.cwd, ctx.framework.dep, ctx.envFile, keylessOptions); return; } } @@ -383,12 +463,46 @@ async function authenticateAndLink( // --- Keyless app setup --- +/** + * A `.clerk/keyless.json` breadcrumb means an earlier run already minted an + * unclaimed keyless application for this project — its claim token, and + * anything configured on it or created in it, only exist as long as that + * breadcrumb (and the env keys pointing at it) survive. Re-running init must + * not silently mint a replacement and orphan that app, so this asks before + * ever touching it: human mode confirms (default: keep); agent mode and `-y` + * both keep it too, since neither can consent to a destructive default. + * `--fresh` is the explicit "I know, replace it anyway" escape hatch. + */ +async function shouldKeepExistingKeyless( + cwd: string, + skipConfirm: boolean, + fresh: boolean, +): Promise { + if (fresh) return false; + + const existing = await readKeylessBreadcrumb(cwd); + if (!existing) return false; + + if (skipConfirm) return true; + + const replace = await confirm({ + message: `This project already has an unclaimed keyless application (created ${existing.createdAt}). Replace it with a new one?`, + default: false, + }); + return !replace; +} + async function setupKeylessApp( cwd: string, frameworkDep: string, envFile: string, - template?: KeylessTemplate, + { template, fresh, skipConfirm }: KeylessRunOptions, ): Promise { + if (await shouldKeepExistingKeyless(cwd, skipConfirm, fresh)) { + printExistingKeylessInfo(envFile); + return; + } + try { const app = await withSpinner( template @@ -517,9 +631,13 @@ export function registerInit(program: Program): void { .addOption( createOption( "--template ", - "Pre-configure the keyless application from a Clerk application template", + "Pre-configure the keyless application from a Clerk application template. Only applies when the strategy resolves to keyless — errors otherwise", ).choices(KEYLESS_TEMPLATES), ) + .option( + "--fresh", + "Replace an existing unclaimed keyless application with a new one, instead of keeping it. Only applies when the strategy resolves to keyless — errors otherwise", + ) .option("-y, --yes", "Skip confirmation prompts") .option("--no-skills", "Skip the optional agent skills install prompt") .setExamples([ @@ -549,6 +667,10 @@ export function registerInit(program: Program): void { command: "clerk init --template b2b-saas", description: "Bootstrap a keyless app pre-configured for B2B SaaS", }, + { + command: "clerk init --keyless --fresh", + description: "Replace an existing unclaimed keyless app with a new one", + }, { command: "clerk init -y", description: "Skip all confirmation prompts" }, { command: "clerk init --no-skills", description: "Skip the agent skills install prompt" }, ]) diff --git a/packages/cli-core/src/commands/init/strategy.test.ts b/packages/cli-core/src/commands/init/strategy.test.ts index a1a93079d..10976d982 100644 --- a/packages/cli-core/src/commands/init/strategy.test.ts +++ b/packages/cli-core/src/commands/init/strategy.test.ts @@ -21,10 +21,13 @@ import { bootstrapMod, keylessMod, } from "../../test/lib/init-harness.ts"; +import * as promptsMod from "../../lib/prompts.ts"; import { init } from "./index.ts"; +const EXISTING_BREADCRUMB = { claimToken: "tok_existing", createdAt: "2024-01-01T00:00:00.000Z" }; + describe("init strategy", () => { - const { setup, setupBootstrapSuccess } = useInitHarness(); + const { setup, setupBootstrapSuccess, track } = useInitHarness(); test("blank dir with keyless framework defaults to keyless when unauthenticated", async () => { setup(); mockBootstrapTo(KEYLESS_CTX); @@ -452,4 +455,243 @@ describe("init strategy", () => { expect(linkMod.link).toHaveBeenCalled(); expect(pullMod.pull).toHaveBeenCalled(); }); + + describe("keeping an existing unclaimed keyless app (defect: re-run minted a second app)", () => { + test("agent mode keeps an existing unclaimed keyless app without prompting or minting a new one", async () => { + setup({ isAgent: true, email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + const breadcrumbSpy = spyOn(keylessMod, "readKeylessBreadcrumb").mockResolvedValue( + EXISTING_BREADCRUMB, + ); + const confirmSpy = spyOn(promptsMod, "confirm"); + const printExistingSpy = spyOn(heuristics, "printExistingKeylessInfo").mockReturnValue( + undefined, + ); + track(breadcrumbSpy); + track(confirmSpy); + track(printExistingSpy); + + await init({}); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(keylessMod.writeKeylessBreadcrumb).not.toHaveBeenCalled(); + expect(printExistingSpy).toHaveBeenCalledWith(KEYLESS_CTX.envFile); + }); + + test("-y keeps an existing unclaimed keyless app without prompting (does not force a fresh one)", async () => { + setup({ email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + const breadcrumbSpy = spyOn(keylessMod, "readKeylessBreadcrumb").mockResolvedValue( + EXISTING_BREADCRUMB, + ); + const confirmSpy = spyOn(promptsMod, "confirm"); + const printExistingSpy = spyOn(heuristics, "printExistingKeylessInfo").mockReturnValue( + undefined, + ); + track(breadcrumbSpy); + track(confirmSpy); + track(printExistingSpy); + + await init({ keyless: true, yes: true }); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + }); + + test("human mode prompts before replacing, defaulting to keep when declined", async () => { + setup({ email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + const breadcrumbSpy = spyOn(keylessMod, "readKeylessBreadcrumb").mockResolvedValue( + EXISTING_BREADCRUMB, + ); + const confirmSpy = spyOn(promptsMod, "confirm").mockResolvedValue(false); + const printExistingSpy = spyOn(heuristics, "printExistingKeylessInfo").mockReturnValue( + undefined, + ); + track(breadcrumbSpy); + track(confirmSpy); + track(printExistingSpy); + + await init({ keyless: true }); + + expect(confirmSpy).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining("already has an unclaimed keyless application"), + default: false, + }), + ); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(printExistingSpy).toHaveBeenCalledWith(KEYLESS_CTX.envFile); + }); + + test("human mode mints a fresh app when the user confirms replacement", async () => { + setup({ email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + const breadcrumbSpy = spyOn(keylessMod, "readKeylessBreadcrumb").mockResolvedValue( + EXISTING_BREADCRUMB, + ); + const confirmSpy = spyOn(promptsMod, "confirm").mockResolvedValue(true); + track(breadcrumbSpy); + track(confirmSpy); + + await init({ keyless: true }); + + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + expect(keylessMod.writeKeylessBreadcrumb).toHaveBeenCalled(); + }); + + test("--fresh mints a new app without prompting or checking for an existing one, even in agent mode", async () => { + setup({ isAgent: true, email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + const breadcrumbSpy = spyOn(keylessMod, "readKeylessBreadcrumb"); + const confirmSpy = spyOn(promptsMod, "confirm"); + track(breadcrumbSpy); + track(confirmSpy); + + await init({ fresh: true }); + + expect(breadcrumbSpy).not.toHaveBeenCalled(); + expect(confirmSpy).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + }); + + test("--fresh with --login throws a usage error before bootstrapping", async () => { + setup(); + + await expect(init({ fresh: true, login: true })).rejects.toThrow( + /--fresh applies to keyless applications/, + ); + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + }); + + test("no existing breadcrumb mints an app normally without prompting", async () => { + setup({ isAgent: true, email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + const breadcrumbSpy = spyOn(keylessMod, "readKeylessBreadcrumb").mockResolvedValue(undefined); + const confirmSpy = spyOn(promptsMod, "confirm"); + track(breadcrumbSpy); + track(confirmSpy); + + await init({}); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + }); + }); + + describe("--template / --fresh silently dropped when the strategy isn't keyless (defect)", () => { + test("--template with CLERK_PLATFORM_API_KEY set errors instead of silently dropping the template", async () => { + setup({ apiKey: true }); + mockExistingProject(KEYLESS_CTX); + + await expect(init({ template: "b2b-saas", yes: true })).rejects.toThrow( + /--template only applies to keyless applications/, + ); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + }); + + test("--fresh with CLERK_PLATFORM_API_KEY set errors instead of silently doing nothing", async () => { + setup({ apiKey: true }); + mockExistingProject(KEYLESS_CTX); + + await expect(init({ fresh: true, yes: true })).rejects.toThrow( + /--fresh only applies to keyless applications/, + ); + }); + + test("--template with --app errors (a real app target always forces the authenticated flow)", async () => { + setup({ email: "user@example.com" }); + spyOn(context, "gatherContext").mockResolvedValue(KEYLESS_CTX); + + await expect(init({ template: "b2b-saas", app: "app_abc", yes: true })).rejects.toThrow( + /--template only applies to keyless applications/, + ); + }); + + test("--template on a non-keyless framework in agent mode names the missing keyless support", async () => { + setup({ isAgent: true, email: "user@example.com" }); + const nonKeylessCtx: FakeCtx = { + ...FAKE_CTX, + existingClerk: false, + framework: { + dep: "vue", + name: "Vue", + sdk: "@clerk/vue", + envVar: "VITE_CLERK_PUBLISHABLE_KEY", + envFile: ".env.local", + }, + envFile: ".env.local", + }; + spyOn(context, "gatherContext").mockResolvedValue(nonKeylessCtx); + + await expect(init({ template: "b2b-saas" })).rejects.toThrow(/does not support keyless mode/); + }); + + test("--template on a keyless-resolved run is still forwarded normally", async () => { + setup(); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ template: "b2b-saas" }); + + expect(keylessMod.createAccountlessApp).toHaveBeenCalledWith( + KEYLESS_CTX.framework.dep, + "b2b-saas", + ); + }); + }); + + describe("agent mode never trusts stored-credential presence over an interactive login hang (defect)", () => { + test("falls back to keyless when the stored credential's presence check would say yes but validation fails", async () => { + // Regression test for the hang: a broken/expired stored session still + // makes hasAccountCredentials() (presence) report true, but agent mode + // has no interactive fallback if that lies — it must validate before + // trusting it, not just check whether *something* is stored. + setup({ isAgent: true, email: null }); + spyOn(heuristics, "isAuthenticated").mockResolvedValue(true); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({}); + + expect(heuristics.isAuthenticated).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + }); + + test("--login validates the credential instead of trusting presence, still erroring on a stale session", async () => { + setup({ isAgent: true, email: null }); + spyOn(heuristics, "isAuthenticated").mockResolvedValue(true); + + await expect(init({ login: true })).rejects.toThrow( + /--login requires an interactive terminal/, + ); + expect(loginMod.login).not.toHaveBeenCalled(); + expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); + }); + + test("a real CLERK_PLATFORM_API_KEY is trusted outright, without needing to validate a stored session", async () => { + process.env.CLERK_PLATFORM_API_KEY = "test_key"; + try { + setup({ isAgent: true, email: null }); + mockExistingProject(KEYLESS_CTX); + spyOn(config, "resolveProfile").mockResolvedValue(undefined); + mockMiddlewareScaffold(); + + await init({}); + + expect(linkMod.link).toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + } finally { + delete process.env.CLERK_PLATFORM_API_KEY; + } + }); + }); }); diff --git a/packages/cli-core/src/commands/open/README.md b/packages/cli-core/src/commands/open/README.md new file mode 100644 index 000000000..770618148 --- /dev/null +++ b/packages/cli-core/src/commands/open/README.md @@ -0,0 +1,62 @@ +# Open Command + +Opens the linked Clerk application's dashboard in your browser. When the current directory isn't linked but holds an unclaimed keyless application instead, opens that application's one-time **claim link** rather than failing. + +## Usage + +```sh +clerk open # Open the linked app's dashboard (development instance) +clerk open users # Open a known subpath +clerk open api-keys +clerk open --print # Print the URL instead of opening a browser +``` + +`open` is Commander's default subcommand for the `open` group — `clerk open [subpath]` and `clerk open dashboard [subpath]` are equivalent. + +## Options + +| Option | Description | +| --------- | ---------------------------------------------------------------------------- | +| `--print` | Print the URL on stdout; don't open a browser or a browser fallback message. | + +## Behavior + +1. Resolves the linked profile for the current directory (`resolveProfile(cwd)` from [`lib/config.ts`](../../lib/config.ts)). +2. **Linked** — builds `{dashboardUrl}/apps/{appId}/instances/{instanceId}/{subpath?}` via `buildDashboardUrl()` and opens it. Always targets the **development** instance; throws `INSTANCE_NOT_FOUND` if the profile has none. +3. **Not linked** — falls through to the keyless path below instead of failing outright. + +An unknown `subpath` (not in [`dashboard-paths.ts`](./dashboard-paths.ts)'s allowlist) is not blocked, just warned about — the CLI opens it anyway since the allowlist can't keep up with every dashboard route. + +### Unclaimed keyless applications + +`clerk link` cannot help a keyless application that has never been claimed — there is no application in any account yet to link to. For that case `open` instead looks for the application's **claim link**, via [`keyless-claim.ts`](./keyless-claim.ts), checking (in order): + +1. `.clerk/.tmp/keyless.json` — an SDK that self-provisioned keys (e.g. `next dev` with none configured) writes its own full `claimUrl` here. +2. `.clerk/keyless.json` — the breadcrumb `clerk init --keyless` writes (see [`lib/keyless.ts`](../../lib/keyless.ts)'s `readKeylessBreadcrumb`), holding just the claim token; the URL is rebuilt as `{dashboardUrl}/apps/claim?token={claimToken}`. + +If a claim link is found, `open` opens/prints/emits **that** URL instead of a dashboard deep-link — an unclaimed app has no `/apps/{appId}/instances/{instanceId}` page to go to. The local secret key (resolved the same way as [`whoami`](../whoami/README.md), via `resolveKeylessTarget()`) is used, best-effort, to look up the instance id/environment type from `GET /v1/instance` purely to decorate the output; a missing or invalid key there never blocks opening the claim link itself. + +A `subpath` cannot be honored for an unclaimed application (there is no dashboard page beyond the claim link yet), so `open users` on an unclaimed app throws instead of silently opening the claim link at the wrong URL: + +``` +"users" isn't reachable yet — this application hasn't been claimed, so it has +no dashboard pages beyond the claim link. Run `clerk open` (no subpath) to +claim it, then retry the subpath once it's linked. +``` + +When **no** claim link can be found at all, the error differs depending on what is on disk, and deliberately does not send the user solely to `clerk link` (a dead end for a genuinely unclaimed app): + +- A secret key exists locally but no claim source does (e.g. `CLERK_SECRET_KEY` set by hand with no `.clerk` files) — names both possibilities: the key may belong to an already-claimed app (`clerk link` / `--app `) or the claim breadcrumb was lost (`clerk init --keyless` regenerates one). +- Nothing at all is found — the original message, pointing at both `clerk link` (if an application already exists in your account) and `clerk init` (to create one). + +| Method | Endpoint | Description | +| ------ | -------------- | ---------------------------------------------------------------------------------------------------------------------------------------- | +| `GET` | `/v1/instance` | Best-effort instance id/environment lookup for display, authenticated with the local secret key. Never blocks the claim link on failure. | + +## Output modes + +- **`--print`** — the bare URL on stdout, nothing else. Works identically for the dashboard deep-link and the keyless claim link. +- **Agent mode** (`isAgent()`) — a JSON object on stdout, `opened: false` either way: + - Linked: `{ url, appId, appName, instanceId, instanceLabel, subpath, opened }`. + - Keyless: `{ url, keyless: true, claimSource, instanceId, environmentType, subpath: null, opened: false }`. +- **Human mode** — `intro`/`outro` framing, the target app or claim-link context on stderr, then attempts `openBrowser()`. On failure, prints the URL as a fallback instead of failing the command. diff --git a/packages/cli-core/src/commands/open/index.test.ts b/packages/cli-core/src/commands/open/index.test.ts index a3d6ce634..56e0ef9c9 100644 --- a/packages/cli-core/src/commands/open/index.test.ts +++ b/packages/cli-core/src/commands/open/index.test.ts @@ -1,13 +1,17 @@ import { test, expect, describe, afterEach, beforeEach, mock } from "bun:test"; import { setMode } from "../../mode.ts"; import { setCurrentEnv } from "../../lib/environment.ts"; -import { useCaptureLog } from "../../test/lib/stubs.ts"; +import { configStubs, useCaptureLog } from "../../test/lib/stubs.ts"; import { isKnownDashboardPath } from "./dashboard-paths.ts"; const mockResolveProfile = mock(); const mockOpenBrowser = mock(); +const mockResolveKeylessTarget = mock(); +const mockFindKeylessClaimUrl = mock(); +const mockDescribeKeylessInstance = mock(); mock.module("../../lib/config.ts", () => ({ + ...configStubs, resolveProfile: (...args: unknown[]) => mockResolveProfile(...args), })); @@ -21,6 +25,15 @@ mock.module("../../lib/spinner.ts", () => ({ pausedOutro: () => {}, })); +mock.module("../../lib/keyless-target.ts", () => ({ + resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), +})); + +mock.module("./keyless-claim.ts", () => ({ + findKeylessClaimUrl: (...args: unknown[]) => mockFindKeylessClaimUrl(...args), + describeKeylessInstance: (...args: unknown[]) => mockDescribeKeylessInstance(...args), +})); + const { openDashboard, buildDashboardUrl } = await import("./index.ts"); const PROFILE = { @@ -85,11 +98,18 @@ describe("openDashboard", () => { setMode("human"); setCurrentEnv("production"); mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "open" }); + // Default to "no keyless application on disk either" so the pre-existing + // not-linked tests exercise the fully-empty case, not the keyless branch. + mockFindKeylessClaimUrl.mockResolvedValue(undefined); + mockResolveKeylessTarget.mockResolvedValue(undefined); }); afterEach(() => { mockResolveProfile.mockReset(); mockOpenBrowser.mockReset(); + mockResolveKeylessTarget.mockReset(); + mockFindKeylessClaimUrl.mockReset(); + mockDescribeKeylessInstance.mockReset(); }); test("human mode: prints arrow + app + dim URL, opens browser", async () => { @@ -186,10 +206,10 @@ describe("openDashboard", () => { ); }); - test("throws NOT_LINKED when no profile", async () => { + test("throws NOT_LINKED when no profile and no keyless application either", async () => { mockResolveProfile.mockResolvedValue(null); - await expect(openDashboard(undefined)).rejects.toThrow(/clerk link/); + await expect(openDashboard(undefined)).rejects.toThrow(/clerk link.*clerk init/is); expect(mockOpenBrowser).not.toHaveBeenCalled(); }); @@ -206,3 +226,117 @@ describe("openDashboard", () => { expect(mockOpenBrowser).not.toHaveBeenCalled(); }); }); + +describe("openDashboard: unclaimed keyless application", () => { + const captured = useCaptureLog(); + + const CLAIM_DESTINATION = { + url: "https://dashboard.clerk.com/apps/claim?token=abc123", + source: ".clerk/keyless.json", + }; + + beforeEach(() => { + setMode("human"); + setCurrentEnv("production"); + mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "open" }); + mockResolveProfile.mockResolvedValue(null); + mockDescribeKeylessInstance.mockResolvedValue({ + instanceId: "ins_keyless123", + environmentType: "development", + }); + }); + + afterEach(() => { + mockResolveProfile.mockReset(); + mockOpenBrowser.mockReset(); + mockResolveKeylessTarget.mockReset(); + mockFindKeylessClaimUrl.mockReset(); + mockDescribeKeylessInstance.mockReset(); + }); + + test("human mode: opens the claim link, not a dashboard deep-link", async () => { + mockFindKeylessClaimUrl.mockResolvedValue(CLAIM_DESTINATION); + mockResolveKeylessTarget.mockResolvedValue({ secretKey: "sk_test_x", source: ".env" }); + + await openDashboard(undefined); + + expect(captured.err).toContain("hasn't been claimed"); + expect(captured.err).toContain("ins_keyless123"); + expect(captured.err).toContain(CLAIM_DESTINATION.url); + expect(mockOpenBrowser).toHaveBeenCalledWith(CLAIM_DESTINATION.url); + }); + + test("--print: prints only the claim URL, no browser", async () => { + mockFindKeylessClaimUrl.mockResolvedValue(CLAIM_DESTINATION); + mockResolveKeylessTarget.mockResolvedValue({ secretKey: "sk_test_x", source: ".env" }); + + await openDashboard(undefined, { print: true }); + + expect(captured.out).toBe(CLAIM_DESTINATION.url); + expect(mockOpenBrowser).not.toHaveBeenCalled(); + }); + + test("agent mode: emits structured JSON with keyless: true, no browser", async () => { + setMode("agent"); + mockFindKeylessClaimUrl.mockResolvedValue(CLAIM_DESTINATION); + mockResolveKeylessTarget.mockResolvedValue({ secretKey: "sk_test_x", source: ".env" }); + + await openDashboard(undefined); + + const payload = JSON.parse(captured.out); + expect(payload).toEqual({ + url: CLAIM_DESTINATION.url, + keyless: true, + claimSource: CLAIM_DESTINATION.source, + instanceId: "ins_keyless123", + environmentType: "development", + subpath: null, + opened: false, + }); + expect(mockOpenBrowser).not.toHaveBeenCalled(); + }); + + test("a bad secret key on disk doesn't block the claim link — instance details just come back null", async () => { + mockFindKeylessClaimUrl.mockResolvedValue(CLAIM_DESTINATION); + mockResolveKeylessTarget.mockRejectedValue(new Error("malformed key")); + + await openDashboard(undefined, { print: true }); + + expect(captured.out).toBe(CLAIM_DESTINATION.url); + }); + + test("subpath is refused instead of opening a dashboard page that doesn't exist yet", async () => { + mockFindKeylessClaimUrl.mockResolvedValue(CLAIM_DESTINATION); + + await expect(openDashboard("users")).rejects.toThrow(/users.*hasn't been claimed/is); + expect(mockOpenBrowser).not.toHaveBeenCalled(); + }); + + test("subpath refusal does not point at `clerk link`", async () => { + mockFindKeylessClaimUrl.mockResolvedValue(CLAIM_DESTINATION); + + let message = ""; + try { + await openDashboard("users"); + } catch (error) { + message = (error as Error).message; + } + expect(message).not.toMatch(/clerk link/); + }); + + test("no claim link found, but a secret key is on disk: doesn't blindly say `clerk link`", async () => { + mockFindKeylessClaimUrl.mockResolvedValue(undefined); + mockResolveKeylessTarget.mockResolvedValue({ secretKey: "sk_test_x", source: ".env.local" }); + + await expect(openDashboard(undefined)).rejects.toThrow(/clerk init --keyless/); + expect(mockOpenBrowser).not.toHaveBeenCalled(); + }); + + test("no claim link and no secret key: falls back to the plain not-linked message", async () => { + mockFindKeylessClaimUrl.mockResolvedValue(undefined); + mockResolveKeylessTarget.mockResolvedValue(undefined); + + await expect(openDashboard(undefined)).rejects.toThrow(/clerk link.*clerk init/is); + expect(mockOpenBrowser).not.toHaveBeenCalled(); + }); +}); diff --git a/packages/cli-core/src/commands/open/index.ts b/packages/cli-core/src/commands/open/index.ts index e8aea8e81..b0f9cb1a8 100644 --- a/packages/cli-core/src/commands/open/index.ts +++ b/packages/cli-core/src/commands/open/index.ts @@ -8,7 +8,9 @@ import { log } from "../../lib/log.ts"; import { bold, cyan, dim } from "../../lib/color.ts"; import { intro, outro } from "../../lib/spinner.ts"; import { isAgent } from "../../mode.ts"; +import { resolveKeylessTarget } from "../../lib/keyless-target.ts"; import { isKnownDashboardPath } from "./dashboard-paths.ts"; +import { describeKeylessInstance, findKeylessClaimUrl } from "./keyless-claim.ts"; interface OpenOptions { print?: boolean; @@ -34,9 +36,7 @@ export async function openDashboard( const resolved = await resolveProfile(cwd); if (!resolved) { - throw new CliError("No Clerk project linked to this directory. Run `clerk link` first.", { - code: ERROR_CODE.NOT_LINKED, - }); + return openKeylessDashboard(cwd, subpath, options); } const { appId, appName } = resolved.profile; @@ -105,6 +105,100 @@ export async function openDashboard( outro(); } +/** + * The keyless counterpart to `openDashboard` above. An unclaimed keyless + * application belongs to no account, so `/apps/{appId}/instances/{instanceId}` + * doesn't exist for it yet — the one page that does is the one-time claim + * link. `clerk link` cannot help here (there is nothing in any account to + * link to), so this path exists precisely so the CLI has *some* answer + * instead of dead-ending on that advice. + */ +async function openKeylessDashboard( + cwd: string, + subpath: string | undefined, + options: OpenOptions, +): Promise { + const destination = await findKeylessClaimUrl(cwd); + + if (!destination) { + // A secret key on disk with no claim link is ambiguous: it may be a + // perfectly claimed app's key set by hand (in which case `clerk link` + // really is the fix), or a keyless app whose breadcrumb got lost. Name + // both possibilities rather than guessing. + const keyless = await resolveKeylessTarget({ cwd }); + if (keyless) { + throw new CliError( + `Found a secret key (from ${keyless.source}) but no claim link on disk, so there's no dashboard page to open yet. ` + + "If this key belongs to an application you've already claimed, run `clerk link` (or pass `--app `) to target it directly. " + + "Otherwise, run `clerk init --keyless` to regenerate a claim link.", + { code: ERROR_CODE.NOT_LINKED }, + ); + } + + throw new CliError( + "No Clerk project linked to this directory, and no keyless application was found either. " + + "Run `clerk link` if you already have an application, or `clerk init` to create one.", + { code: ERROR_CODE.NOT_LINKED }, + ); + } + + if (subpath) { + throw new CliError( + `"${subpath}" isn't reachable yet — this application hasn't been claimed, so it has no dashboard pages beyond the claim link. ` + + "Run `clerk open` (no subpath) to claim it, then retry the subpath once it's linked.", + { code: ERROR_CODE.NOT_LINKED }, + ); + } + + const { url, source } = destination; + + // Best-effort only: the claim link is already fully formed, so a bad or + // missing secret key shouldn't block opening it — it just means the + // output won't include instance details. + const instance = await resolveKeylessTarget({ cwd }) + .then((keyless) => (keyless ? describeKeylessInstance(keyless.secretKey) : null)) + .catch(() => null); + + // Output strategy mirrors the linked-app flow above: + // --print → plain URL on stdout (scriptable) + // agent mode → JSON object with full context (parseable) + // human mode → intro/outro logging flow with browser open + if (options.print) { + log.data(url); + return; + } + + if (isAgent()) { + log.data( + JSON.stringify({ + url, + keyless: true, + claimSource: source, + instanceId: instance?.instanceId ?? null, + environmentType: instance?.environmentType ?? null, + subpath: null, + opened: false, + }), + ); + return; + } + + intro("Opening dashboard"); + + const suffix = instance?.instanceId ? ` (${instance.instanceId})` : ""; + log.info(`↗ This application hasn't been claimed yet — opening its claim link${suffix}`); + log.info(` ${dim(url)}`); + + const result = await openBrowser(url); + if (!result.ok) { + log.warn( + `Could not open your browser automatically. Open this URL to continue:\n ${cyan(url)}\n${dim(`(Reason: ${result.reason})`)}`, + ); + } + + outro(); +} + export function registerOpen(program: Program): void { const open = program.command("open").description("Open Clerk resources in your browser"); diff --git a/packages/cli-core/src/commands/open/keyless-claim.test.ts b/packages/cli-core/src/commands/open/keyless-claim.test.ts new file mode 100644 index 000000000..549b7929e --- /dev/null +++ b/packages/cli-core/src/commands/open/keyless-claim.test.ts @@ -0,0 +1,145 @@ +import { test, expect, describe, beforeEach, afterEach } from "bun:test"; +import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { setCurrentEnv } from "../../lib/environment.ts"; +import { stubFetch } from "../../test/lib/stubs.ts"; +import { describeKeylessInstance, findKeylessClaimUrl } from "./keyless-claim.ts"; + +describe("findKeylessClaimUrl", () => { + let projectDir: string; + const originalFetch = globalThis.fetch; + + beforeEach(async () => { + projectDir = await mkdtemp(join(tmpdir(), "clerk-open-keyless-")); + setCurrentEnv("production"); + }); + + afterEach(async () => { + globalThis.fetch = originalFetch; + await rm(projectDir, { recursive: true, force: true }); + }); + + test("returns undefined when neither file exists", async () => { + expect(await findKeylessClaimUrl(projectDir)).toBeUndefined(); + }); + + test("reads the full claimUrl an SDK wrote for itself", async () => { + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", ".tmp", "keyless.json"), + JSON.stringify({ claimUrl: "https://dashboard.clerk.com/apps/claim?token=sdk-token" }), + ); + + expect(await findKeylessClaimUrl(projectDir)).toEqual({ + url: "https://dashboard.clerk.com/apps/claim?token=sdk-token", + source: ".clerk/.tmp/keyless.json", + }); + }); + + test("rebuilds the URL from the CLI's own breadcrumb token", async () => { + await mkdir(join(projectDir, ".clerk"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", "keyless.json"), + JSON.stringify({ claimToken: "cli-token", createdAt: new Date().toISOString() }), + ); + + expect(await findKeylessClaimUrl(projectDir)).toEqual({ + url: "https://dashboard.clerk.com/apps/claim?token=cli-token", + source: ".clerk/keyless.json", + }); + }); + + test("prefers the SDK's own claimUrl over the CLI breadcrumb", async () => { + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", ".tmp", "keyless.json"), + JSON.stringify({ claimUrl: "https://dashboard.clerk.com/apps/claim?token=sdk-token" }), + ); + await writeFile( + join(projectDir, ".clerk", "keyless.json"), + JSON.stringify({ claimToken: "cli-token", createdAt: new Date().toISOString() }), + ); + + const destination = await findKeylessClaimUrl(projectDir); + expect(destination?.source).toBe(".clerk/.tmp/keyless.json"); + }); + + test("falls back to the breadcrumb when the SDK file has no claimUrl", async () => { + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", ".tmp", "keyless.json"), + JSON.stringify({ secretKey: "sk_test_x" }), + ); + await mkdir(join(projectDir, ".clerk"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", "keyless.json"), + JSON.stringify({ claimToken: "cli-token", createdAt: new Date().toISOString() }), + ); + + const destination = await findKeylessClaimUrl(projectDir); + expect(destination?.source).toBe(".clerk/keyless.json"); + }); + + test("ignores a malformed SDK keyless file and falls back to the breadcrumb", async () => { + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile(join(projectDir, ".clerk", ".tmp", "keyless.json"), "{ not json"); + await writeFile( + join(projectDir, ".clerk", "keyless.json"), + JSON.stringify({ claimToken: "cli-token", createdAt: new Date().toISOString() }), + ); + + expect(await findKeylessClaimUrl(projectDir)).toEqual({ + url: "https://dashboard.clerk.com/apps/claim?token=cli-token", + source: ".clerk/keyless.json", + }); + }); +}); + +describe("describeKeylessInstance", () => { + const originalFetch = globalThis.fetch; + + beforeEach(() => { + setCurrentEnv("production"); + }); + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + test("returns instance id and environment type on success", async () => { + stubFetch(async () => + Response.json({ id: "ins_abc", object: "instance", environment_type: "development" }), + ); + + expect(await describeKeylessInstance("sk_test_x")).toEqual({ + instanceId: "ins_abc", + environmentType: "development", + }); + }); + + test("returns nulls instead of throwing when BAPI rejects the key", async () => { + stubFetch( + async () => + new Response(JSON.stringify({ errors: [{ message: "unauthorized" }] }), { + status: 401, + }), + ); + + expect(await describeKeylessInstance("sk_test_bad")).toEqual({ + instanceId: null, + environmentType: null, + }); + }); + + test("returns nulls instead of throwing on a network failure", async () => { + stubFetch(async () => { + throw new Error("network down"); + }); + + expect(await describeKeylessInstance("sk_test_x")).toEqual({ + instanceId: null, + environmentType: null, + }); + }); +}); diff --git a/packages/cli-core/src/commands/open/keyless-claim.ts b/packages/cli-core/src/commands/open/keyless-claim.ts new file mode 100644 index 000000000..609ee9e83 --- /dev/null +++ b/packages/cli-core/src/commands/open/keyless-claim.ts @@ -0,0 +1,99 @@ +/** + * Where `clerk open` sends an unclaimed keyless application. + * + * A keyless app belongs to no account yet, so the normal dashboard deep-link + * (`/apps/{appId}/instances/{instanceId}/...`) 404s — there is no appId to + * put in it. The only page that reliably exists for it is the one-time claim + * link, which is why this module looks for that instead of reusing + * `buildDashboardUrl`. + */ + +import { join } from "node:path"; +import { readKeylessBreadcrumb } from "../../lib/keyless.ts"; +import { getDashboardUrl } from "../../lib/environment.ts"; +import { bapiRequest } from "../../lib/bapi.ts"; +import { log } from "../../lib/log.ts"; + +export interface KeylessClaimDestination { + url: string; + /** Where the claim link came from, for display (mirrors `KeylessTarget.source`). */ + source: string; +} + +/** Best-effort instance metadata for display only — never blocks the claim link. */ +export interface KeylessInstanceInfo { + instanceId: string | null; + environmentType: string | null; +} + +const SDK_KEYLESS_FILE = [".clerk", ".tmp", "keyless.json"]; + +/** + * The SDKs write their own full `claimUrl` (already pointed at the right + * dashboard host) into this file. Reading it directly here — rather than + * through `lib/keyless-target.ts` — because that module only surfaces the + * key pair it needs for BAPI calls, not the claim URL. + */ +async function readSdkClaimUrl(cwd: string): Promise { + const file = Bun.file(join(cwd, ...SDK_KEYLESS_FILE)); + if (!(await file.exists())) return undefined; + + try { + const parsed = (await file.json()) as { claimUrl?: unknown }; + return typeof parsed.claimUrl === "string" ? parsed.claimUrl : undefined; + } catch { + // A half-written file during an SDK refresh isn't worth failing over. + return undefined; + } +} + +/** + * Finds the claim link for an unclaimed keyless application, checking both + * places one can turn up depending on how keyless mode was entered: + * + * - `clerk init --keyless` writes only the claim TOKEN to `.clerk/keyless.json` + * (see `writeKeylessBreadcrumb`); the URL is rebuilt against whichever + * dashboard host the CLI is currently pointed at. + * - An SDK that self-provisions (e.g. `next dev` with no keys configured) + * writes the full URL straight into `.clerk/.tmp/keyless.json`, so that one + * is used verbatim rather than reconstructed. + * + * Returns `undefined` when neither file has a usable claim link. + */ +export async function findKeylessClaimUrl( + cwd: string, +): Promise { + const sdkClaimUrl = await readSdkClaimUrl(cwd); + if (sdkClaimUrl) { + return { url: sdkClaimUrl, source: SDK_KEYLESS_FILE.join("/") }; + } + + const breadcrumb = await readKeylessBreadcrumb(cwd); + if (breadcrumb) { + const host = getDashboardUrl().replace(/\/$/, ""); + return { + url: `${host}/apps/claim?token=${breadcrumb.claimToken}`, + source: ".clerk/keyless.json", + }; + } + + return undefined; +} + +/** + * Enriches the claim link with the instance's own id/environment, purely for + * display. BAPI has no route that returns claim info for an existing + * instance (only the one-time creation response does), so this can only ever + * decorate a destination already found on disk — never produce one itself. + * Failures here must not block opening the claim link, hence the catch. + */ +export async function describeKeylessInstance(secretKey: string): Promise { + try { + const response = await bapiRequest({ method: "GET", path: "/v1/instance", secretKey }); + const body = response.body as { id?: string; environment_type?: string }; + return { instanceId: body.id ?? null, environmentType: body.environment_type ?? null }; + } catch (error) { + log.debug(`open: could not fetch instance info for keyless app (${(error as Error).message})`); + return { instanceId: null, environmentType: null }; + } +} diff --git a/packages/cli-core/src/commands/users/README.md b/packages/cli-core/src/commands/users/README.md index c6ee420b8..812c5cb0f 100644 --- a/packages/cli-core/src/commands/users/README.md +++ b/packages/cli-core/src/commands/users/README.md @@ -2,6 +2,11 @@ Manage direct Clerk user resources with first-class commands. Use `clerk api` for unsupported or fully custom user requests. +Works with no login and no linked project on an **unclaimed keyless +application** — the one an SDK creates for itself the first time you run +`next dev` (or similar) with no keys configured. See +[Shared Targeting And Auth](#shared-targeting-and-auth) below. + ## Shared Targeting And Auth Most `clerk users` commands accept the same targeting flags: @@ -16,12 +21,14 @@ Most `clerk users` commands accept the same targeting flags: Authentication is resolved in this order: -- `--app ` plus Platform API auth to resolve the instance secret key -- `--secret-key ` -- `CLERK_SECRET_KEY` +- `--secret-key ` (explicit) +- `--app ` plus Platform API auth to resolve the instance secret key (explicit) +- this project's own keyless secret key — `CLERK_SECRET_KEY`, `.env.local` (or the framework's detected env var name), or the SDK's own `.clerk/.tmp/keyless.json` - a linked project profile via `clerk link` -The users commands talk to the instance's Backend API. Identifier and required-field rules are enforced by BAPI, so any BAPI secret key (via `CLERK_SECRET_KEY`, `--secret-key`, or `--app`-resolved) is enough — no `applications:manage` Platform API scope is required. +The third step is what makes `clerk users` work with no login and no Platform API auth on an **unclaimed keyless application** — the one an SDK creates for itself on first `next dev` (or similar) with no keys configured. It only applies when the directory isn't linked and `--app` wasn't passed, since either of those names an explicit destination the on-disk key might not belong to. + +The users commands talk to the instance's Backend API. Identifier and required-field rules are enforced by BAPI, so any BAPI secret key (via `CLERK_SECRET_KEY`, `.env.local`, `--secret-key`, or `--app`-resolved) is enough — no `applications:manage` Platform API scope is required. ## Interactive mode @@ -119,6 +126,12 @@ In agent mode the user-id is required (no interactive picker) and output is a JS `--secret-key` chooses the Backend API key used for user lookup. `users open` still requires an app target to resolve the dashboard URL, either from `--app`, a linked project, or the human-mode app picker. Use `--instance` when you want something other than the default development instance. +#### On an unclaimed keyless application + +This is the one command in the family that an unclaimed keyless application cannot satisfy, and the reason is structural rather than a missing code path: a dashboard link is `/apps/{appId}/instances/{instanceId}/users/{userId}`, and an application has no `appId` until somebody claims it. + +Rather than report the generic "no Clerk project linked", `users open` detects the keyless project and says so — because the two remedies that message implies, `clerk link` and `--app`, both want an application ID that does not exist yet. The error names `clerk auth login` to claim the application, and `clerk api /users/` to read the user right now without claiming anything. Passing `--app` explicitly skips this check: naming an application says the request isn't about the keyless one in this directory. + ## API Endpoints | Method | Endpoint | Command(s) | diff --git a/packages/cli-core/src/commands/users/open.test.ts b/packages/cli-core/src/commands/users/open.test.ts index 58eb2dc57..d520a279e 100644 --- a/packages/cli-core/src/commands/users/open.test.ts +++ b/packages/cli-core/src/commands/users/open.test.ts @@ -1,12 +1,15 @@ import { test, expect, describe, afterEach, beforeEach, mock } from "bun:test"; import { setMode } from "../../mode.ts"; import { setCurrentEnv } from "../../lib/environment.ts"; -import { useCaptureLog } from "../../test/lib/stubs.ts"; +import { configStubs, useCaptureLog } from "../../test/lib/stubs.ts"; const mockResolveAppContext = mock(); const mockResolveProfile = mock(); const mockResolveInstanceId = mock(); +// Spread the full stub set: a partial module mock only stays valid while +// nothing else in the import graph reaches for the exports it left out. mock.module("../../lib/config.ts", () => ({ + ...configStubs, resolveAppContext: (...args: unknown[]) => mockResolveAppContext(...args), resolveProfile: (...args: unknown[]) => mockResolveProfile(...args), resolveInstanceId: (...args: unknown[]) => mockResolveInstanceId(...args), @@ -27,6 +30,11 @@ mock.module("../../lib/open.ts", () => ({ openBrowser: (...args: unknown[]) => mockOpenBrowser(...args), })); +const mockResolveKeylessTarget = mock(); +mock.module("../../lib/keyless-target.ts", () => ({ + resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), +})); + mock.module("../../lib/spinner.ts", () => ({ intro: () => {}, outro: () => {}, @@ -63,6 +71,7 @@ describe("users open", () => { }); mockResolveUsersInstanceContext.mockResolvedValue(CTX); mockOpenBrowser.mockResolvedValue({ ok: true, launcher: "open" }); + mockResolveKeylessTarget.mockResolvedValue(undefined); }); afterEach(() => { @@ -72,6 +81,43 @@ describe("users open", () => { mockResolveUsersInstanceContext.mockReset(); mockPickUser.mockReset(); mockOpenBrowser.mockReset(); + mockResolveKeylessTarget.mockReset(); + }); + + describe("on an unclaimed keyless application", () => { + beforeEach(() => { + // Nothing linked and no account context — the state `clerk init` leaves + // behind before anyone claims the app. + mockResolveAppContext.mockRejectedValue( + new Error("No Clerk project linked to this directory."), + ); + mockResolveKeylessTarget.mockResolvedValue({ + secretKey: "sk_test_keyless", + source: ".env.local", + }); + }); + + test("says the application is unclaimed rather than that nothing is linked", async () => { + await expect(open({ userId: "user_abc" })).rejects.toThrow( + /unclaimed keyless application \(secret key from \.env\.local\).*no Dashboard page/s, + ); + }); + + test("suggests claiming, and a command that works right now", async () => { + const error = await open({ userId: "user_abc" }).catch((thrown: unknown) => thrown); + // `clerk users get` does not exist; `clerk api` is the reader that does. + expect((error as Error).message).toContain("clerk auth login"); + expect((error as Error).message).toContain("clerk api /users/user_abc"); + }); + + test("does not hijack an explicit --app target", async () => { + // Naming an application says the user isn't asking about the keyless one + // sitting in this directory, so that resolution must run untouched. + await open({ userId: "user_abc", app: "app_other", print: true }); + + expect(mockResolveKeylessTarget).not.toHaveBeenCalled(); + expect(captured.out).toContain(`/apps/${CTX.appId}`); + }); }); test("explicit user-id + linked profile: opens dashboard URL for that user", async () => { diff --git a/packages/cli-core/src/commands/users/open.ts b/packages/cli-core/src/commands/users/open.ts index 0393dcf43..98309abfc 100644 --- a/packages/cli-core/src/commands/users/open.ts +++ b/packages/cli-core/src/commands/users/open.ts @@ -1,6 +1,7 @@ import { bold, cyan, dim } from "../../lib/color.ts"; import { resolveAppContext, resolveInstanceId, resolveProfile } from "../../lib/config.ts"; import { CliError, ERROR_CODE, throwUsageError } from "../../lib/errors.ts"; +import { resolveKeylessTarget } from "../../lib/keyless-target.ts"; import { log } from "../../lib/log.ts"; import { openBrowser } from "../../lib/open.ts"; import { intro, outro } from "../../lib/spinner.ts"; @@ -64,6 +65,31 @@ async function resolveKnownUserDashboardTarget(options: UsersOpenOptions): Promi }; } +/** + * Fails with the real reason when the directory holds an unclaimed keyless + * application, rather than letting the caller land on `not_linked`. + * + * Both are true — nothing is linked, and there is nothing to link to — but only + * one of them tells you what to do next. `clerk link` and `--app` both expect an + * application ID that an unclaimed application does not have yet, so the generic + * message sends people to try two things that cannot work. + */ +async function assertNotUnclaimedKeyless(options: UsersOpenOptions, userId: string): Promise { + // An explicit destination means the user is not asking about keyless at all, + // and the original error is the accurate one. + if (options.app || options.instance) return; + + const keyless = await resolveKeylessTarget({ cwd: process.cwd() }); + if (!keyless) return; + + throw new CliError( + `This directory holds an unclaimed keyless application (secret key from ${keyless.source}), which has no Dashboard page — a dashboard link needs an application ID, and one is only assigned when the application is claimed.\n` + + `Run \`clerk auth login\` to claim it, then \`clerk users open ${userId}\` will work.\n` + + `To inspect the user right now, \`clerk api /users/${userId}\` reads it straight from the instance.`, + { code: ERROR_CODE.INSTANCE_NOT_FOUND }, + ); +} + export async function open(options: UsersOpenOptions = {}): Promise { let userId = options.userId; if (userId !== undefined && !/^user_[A-Za-z0-9]+$/.test(userId)) { @@ -83,6 +109,13 @@ export async function open(options: UsersOpenOptions = {}): Promise { try { target = await resolveKnownUserDashboardTarget(options); } catch (error) { + // An unclaimed application has no Dashboard page to open — the deep-link + // is `/apps/{appId}/…` and there is no appId until somebody claims it. + // Checked before the `--secret-key` branch below because the advice that + // branch would eventually give ("use --app", "run `clerk link`") is + // unreachable here: no app exists to name. + await assertNotUnclaimedKeyless(options, userId); + if (!options.secretKey) { throw error; } diff --git a/packages/cli-core/src/commands/whoami/README.md b/packages/cli-core/src/commands/whoami/README.md index 995ed0073..7e7a29932 100644 --- a/packages/cli-core/src/commands/whoami/README.md +++ b/packages/cli-core/src/commands/whoami/README.md @@ -26,7 +26,7 @@ clerk whoami --json ### Keyless applications -An unclaimed keyless application has no account to name, so the instance itself is the identity. When there's no stored token but the directory holds an instance secret key (env var, `.env`/`.env.local`, or `.clerk/.tmp/keyless.json` — see [`lib/keyless-target.ts`](../../lib/keyless-target.ts)), `whoami` reads `GET /v1/instance` with that key and reports the instance instead of erroring. +An unclaimed keyless application has no account to name, so the instance itself is the identity. When there's no stored token but the directory holds an instance secret key (env var, `.env`/`.env.local`, or `.clerk/.tmp/keyless.json` — see [`lib/keyless-target.ts`](../../lib/keyless-target.ts)), `whoami` reads `GET /v1/instance` with that key and reports the instance instead of erroring. A secret key the API rejects (revoked, malformed) surfaces as an error naming the key and where it came from, not a bare "Request failed (401)". ```json { @@ -35,17 +35,21 @@ An unclaimed keyless application has no account to name, so the instance itself "instanceId": "ins_...", "environmentType": "development", "publishableKey": "pk_test_...", + "publishableKeyMismatch": false, "keySource": ".clerk/.tmp/keyless.json" }, "linked": null } ``` +`publishableKey` and the secret key are found independently (see `findLocalSecretKey`/`findLocalPublishableKey` in `lib/keyless-target.ts`) and can each belong to a _different_ keyless application if a project's env files hold leftovers from more than one. `publishableKeyMismatch` is `true` when that's happened — checked by decoding the publishable key's Frontend API host (`decodePublishableKey` in `lib/fapi.ts`) and confirming it appears in the secret key's own `GET /v1/domains`. Whoami only warns in this case (on **stderr**) and still reports what it found; `clerk env pull` is the stricter, refusing to write the mismatched pair (see [`commands/env/README.md`](../env/README.md)). + In human mode the instance ID goes to **stdout** and the explanation (including where the key came from) to **stderr**. -| Method | Endpoint | Description | -| ------ | -------------- | ------------------------------------------------------------------------- | -| `GET` | `/v1/instance` | Reads the keyless instance's identity. Authenticated with the secret key. | +| Method | Endpoint | Description | +| ------ | -------------- | ---------------------------------------------------------------------------------------------- | +| `GET` | `/v1/instance` | Reads the keyless instance's identity. Authenticated with the secret key. | +| `GET` | `/v1/domains` | Only when a local publishable key was found — checks it against the secret key's own instance. | - If the token is expired or invalid, throws an `AuthError` ("Session expired"). diff --git a/packages/cli-core/src/commands/whoami/index.test.ts b/packages/cli-core/src/commands/whoami/index.test.ts index 5390ae4c8..214a8a628 100644 --- a/packages/cli-core/src/commands/whoami/index.test.ts +++ b/packages/cli-core/src/commands/whoami/index.test.ts @@ -8,13 +8,29 @@ import { import { CliError } from "../../lib/errors.ts"; const mockGetValidToken = mock(); +const mockHasStoredCredentials = mock(); const mockFetchUserInfo = mock(); const mockResolveProfile = mock(); const mockIsAgent = mock(); +const mockResolveKeylessTarget = mock(); +const mockFindLocalPublishableKey = mock(); +const mockHasKeyPairMismatch = mock(); +const mockBapiRequest = mock(); mock.module("../../lib/credential-store.ts", () => ({ ...credentialStoreStubs, getValidToken: (...args: unknown[]) => mockGetValidToken(...args), + hasStoredCredentials: (...args: unknown[]) => mockHasStoredCredentials(...args), +})); + +mock.module("../../lib/keyless-target.ts", () => ({ + resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), + findLocalPublishableKey: (...args: unknown[]) => mockFindLocalPublishableKey(...args), + hasKeyPairMismatch: (...args: unknown[]) => mockHasKeyPairMismatch(...args), +})); + +mock.module("../../lib/bapi.ts", () => ({ + bapiRequest: (...args: unknown[]) => mockBapiRequest(...args), })); mock.module("../../lib/token-exchange.ts", () => ({ @@ -54,13 +70,22 @@ describe("whoami", () => { beforeEach(() => { mockIsAgent.mockReturnValue(false); mockResolveProfile.mockResolvedValue(undefined); + mockHasStoredCredentials.mockResolvedValue(false); + mockResolveKeylessTarget.mockResolvedValue(undefined); + mockFindLocalPublishableKey.mockResolvedValue(undefined); + mockHasKeyPairMismatch.mockResolvedValue(false); }); afterEach(() => { mockGetValidToken.mockReset(); + mockHasStoredCredentials.mockReset(); mockFetchUserInfo.mockReset(); mockResolveProfile.mockReset(); mockIsAgent.mockReset(); + mockResolveKeylessTarget.mockReset(); + mockFindLocalPublishableKey.mockReset(); + mockHasKeyPairMismatch.mockReset(); + mockBapiRequest.mockReset(); consoleSpy?.mockRestore(); }); @@ -216,4 +241,128 @@ describe("whoami", () => { expect(payload.linked.appId).toBe("app_xxx"); expect(captured.err).not.toContain("Linked to"); }); + + describe("keyless", () => { + const KEYLESS_TARGET = { secretKey: "sk_test_keyless", source: ".env.local" }; + + /** Points the keyless fallback at an instance the Backend API will answer for. */ + function withKeylessProject(): void { + mockResolveKeylessTarget.mockResolvedValue(KEYLESS_TARGET); + mockFindLocalPublishableKey.mockResolvedValue("pk_test_keyless"); + mockBapiRequest.mockResolvedValue({ + body: { id: "ins_keyless_1", environment_type: "development" }, + }); + } + + test("reports the keyless instance when there is no account at all", async () => { + mockGetValidToken.mockResolvedValue(null); + withKeylessProject(); + + await runWhoami({ json: true }); + + expect(JSON.parse(captured.out)).toEqual({ + email: null, + linked: null, + keyless: { + instanceId: "ins_keyless_1", + environmentType: "development", + publishableKey: "pk_test_keyless", + publishableKeyMismatch: false, + keySource: ".env.local", + }, + }); + }); + + test("warns and flags the JSON payload when the local publishable key belongs to a different app", async () => { + mockGetValidToken.mockResolvedValue(null); + withKeylessProject(); + mockHasKeyPairMismatch.mockResolvedValue(true); + + await runWhoami({ json: true }); + + expect(JSON.parse(captured.out)).toMatchObject({ + keyless: { publishableKeyMismatch: true }, + }); + expect(captured.err).toContain("doesn't belong to this secret key's application"); + }); + + test("a failed pairing check doesn't block reporting the identity", async () => { + mockGetValidToken.mockResolvedValue(null); + withKeylessProject(); + mockHasKeyPairMismatch.mockRejectedValue(new Error("network blip")); + + await runWhoami({ json: true }); + + expect(JSON.parse(captured.out)).toMatchObject({ + keyless: { instanceId: "ins_keyless_1", publishableKeyMismatch: false }, + }); + }); + + test("no pairing check runs when no local publishable key is found", async () => { + mockGetValidToken.mockResolvedValue(null); + withKeylessProject(); + mockFindLocalPublishableKey.mockResolvedValue(undefined); + + await runWhoami({ json: true }); + + expect(mockHasKeyPairMismatch).not.toHaveBeenCalled(); + expect(JSON.parse(captured.out)).toMatchObject({ + keyless: { publishableKey: null, publishableKeyMismatch: false }, + }); + }); + + test("wraps an invalid keyless secret key with context naming the key and its source", async () => { + mockGetValidToken.mockResolvedValue(null); + mockResolveKeylessTarget.mockResolvedValue(KEYLESS_TARGET); + const { BapiError } = await import("../../lib/errors.ts"); + mockBapiRequest.mockRejectedValue( + new BapiError(401, '{"errors":[{"message":"invalid key"}]}', new Headers()), + ); + + const error = await runWhoami({ json: true }).catch((e: unknown) => e); + + expect((error as { context?: string }).context).toContain(".env.local"); + }); + + // A login whose refresh token the server has since rejected used to abort + // whoami outright, because the "am I signed in?" question threw instead of + // answering no. + test("falls back to keyless when the stored session can no longer be refreshed", async () => { + mockGetValidToken.mockRejectedValue(new CliError("Token refresh failed (401)")); + mockHasStoredCredentials.mockResolvedValue(true); + withKeylessProject(); + + await runWhoami({ json: true }); + + expect(JSON.parse(captured.out)).toMatchObject({ + email: null, + keyless: { instanceId: "ins_keyless_1" }, + }); + expect(mockFetchUserInfo).not.toHaveBeenCalled(); + }); + + test("an unrefreshable session with no keyless keys still reports an expired session", async () => { + mockGetValidToken.mockRejectedValue(new CliError("Token refresh failed (401)")); + mockHasStoredCredentials.mockResolvedValue(true); + + await expect(runWhoami()).rejects.toThrow(/Session expired/); + }); + + test("no credentials and no keyless keys reports not logged in", async () => { + mockGetValidToken.mockResolvedValue(null); + + await expect(runWhoami()).rejects.toThrow(/Not logged in/); + }); + + test("human output names the instance and where its key came from", async () => { + mockGetValidToken.mockResolvedValue(null); + withKeylessProject(); + + await runWhoami(); + + expect(captured.out.trim()).toBe("ins_keyless_1"); + expect(captured.err).toContain("unclaimed keyless application"); + expect(captured.err).toContain(".env.local"); + }); + }); }); diff --git a/packages/cli-core/src/commands/whoami/index.ts b/packages/cli-core/src/commands/whoami/index.ts index 2b3de6401..4ece7d6f5 100644 --- a/packages/cli-core/src/commands/whoami/index.ts +++ b/packages/cli-core/src/commands/whoami/index.ts @@ -1,15 +1,16 @@ import type { Program } from "../../cli-program.ts"; -import { getValidToken } from "../../lib/credential-store.ts"; +import { getValidToken, hasStoredCredentials } from "../../lib/credential-store.ts"; import { fetchUserInfo } from "../../lib/token-exchange.ts"; import { withSpinner } from "../../lib/spinner.ts"; import { log } from "../../lib/log.ts"; -import { AuthError } from "../../lib/errors.ts"; +import { AuthError, errorMessage, withApiContext } from "../../lib/errors.ts"; import { profileLabel, resolveProfile } from "../../lib/config.ts"; import { NEXT_STEPS, printNextSteps } from "../../lib/next-steps.ts"; import { isAgent } from "../../mode.ts"; import { bapiRequest } from "../../lib/bapi.ts"; import { findLocalPublishableKey, + hasKeyPairMismatch, resolveKeylessTarget, type KeylessTarget, } from "../../lib/keyless-target.ts"; @@ -30,6 +31,8 @@ type Identity = instanceId: string | null; environmentType: string | null; publishableKey: string | null; + /** True when `publishableKey` was found locally but doesn't address this secret key's own instance. */ + publishableKeyMismatch: boolean; keySource: string; }; @@ -45,13 +48,27 @@ export async function whoami(options: WhoamiOptions = {}) { } async function resolveIdentity(): Promise { - const token = await getValidToken(); + // Stored credentials that can no longer be refreshed are, for this command's + // purposes, the same as no credentials: there is no account to report. Ask the + // question in a way that can't throw, so an expired session falls through to + // the keyless path instead of aborting it. + const token = await getValidToken().catch((error: unknown) => { + log.debug(`credentials: stored session unusable (${errorMessage(error)})`); + return null; + }); - // No account, but the directory may still hold a working keyless + // No usable account, but the directory may still hold a working keyless // application. Report what it is instead of a flat "not logged in". if (!token) { const keyless = await resolveKeylessTarget({ cwd: process.cwd() }); - if (!keyless) throw new AuthError({ reason: "not_logged_in" }); + if (!keyless) { + // Nothing to fall back to, so the distinction matters again: a session + // that expired is fixed by logging in again, not by logging in for the + // first time. + throw new AuthError({ + reason: (await hasStoredCredentials()) ? "session_expired" : "not_logged_in", + }); + } return describeKeyless(keyless); } @@ -75,23 +92,53 @@ async function resolveIdentity(): Promise { async function describeKeyless(keyless: KeylessTarget): Promise { const instance = await withSpinner("Fetching instance info...", async () => { - const response = await bapiRequest({ - method: "GET", - path: "/v1/instance", - secretKey: keyless.secretKey, - }); + const response = await withApiContext( + bapiRequest({ method: "GET", path: "/v1/instance", secretKey: keyless.secretKey }), + `Failed to read the keyless secret key from \`${keyless.source}\``, + ); return response.body as { id?: string; environment_type?: string }; }); + const publishableKey = (await findLocalPublishableKey(process.cwd())) ?? null; + const publishableKeyMismatch = publishableKey + ? await checkKeyPairMismatch(keyless, publishableKey) + : false; + + if (publishableKeyMismatch) { + log.warn( + `The publishable key found locally doesn't belong to this secret key's application — the server (secret key from \`${keyless.source}\`) and the browser (\`${publishableKey}\`) would be talking to different apps.\n` + + "Check your env files for a leftover key from another project, or run `clerk env pull` once they match.", + ); + } + return { kind: "keyless", instanceId: instance.id ?? null, environmentType: instance.environment_type ?? null, - publishableKey: (await findLocalPublishableKey(process.cwd())) ?? null, + publishableKey, + publishableKeyMismatch, keySource: keyless.source, }; } +/** + * Whoami's job here is to report, not to block — a pairing check that itself + * fails (network hiccup on `/v1/domains`, say) shouldn't stop it from showing + * the identity `/v1/instance` already confirmed. `env pull` is the write path + * and is the stricter of the two: see `commands/env/pull.ts`. + */ +async function checkKeyPairMismatch( + keyless: KeylessTarget, + publishableKey: string, +): Promise { + try { + return await hasKeyPairMismatch(keyless, publishableKey); + } catch (error) { + log.debug(`whoami: key pairing check failed (${errorMessage(error)})`); + return false; + } +} + function toJson(identity: Identity): Record { if (identity.kind === "keyless") { const { kind: _kind, ...keyless } = identity; diff --git a/packages/cli-core/src/lib/bapi-command.test.ts b/packages/cli-core/src/lib/bapi-command.test.ts index e7058668b..b4fa6cf25 100644 --- a/packages/cli-core/src/lib/bapi-command.test.ts +++ b/packages/cli-core/src/lib/bapi-command.test.ts @@ -4,6 +4,7 @@ import { useCaptureLog } from "../test/lib/stubs.ts"; const configModule = await import("./config.ts"); const plapiModule = await import("./plapi.ts"); +const keylessTargetModule = await import("./keyless-target.ts"); const { normalizeBapiPath, handleBapiError, resolveBapiSecretKey, describeBapiTarget } = await import("./bapi-command.ts"); @@ -12,6 +13,7 @@ describe("bapi-command", () => { let resolveAppContextSpy: ReturnType; let fetchApplicationSpy: ReturnType; let validateKeyPrefixSpy: ReturnType; + let resolveKeylessTargetSpy: ReturnType; const captured = useCaptureLog(); beforeEach(() => { @@ -19,6 +21,11 @@ describe("bapi-command", () => { resolveAppContextSpy = spyOn(configModule, "resolveAppContext"); fetchApplicationSpy = spyOn(plapiModule, "fetchApplication"); validateKeyPrefixSpy = spyOn(plapiModule, "validateKeyPrefix"); + // Defaults to "no keyless project here" so existing account-path tests are + // unaffected; individual tests override this to exercise the keyless path. + resolveKeylessTargetSpy = spyOn(keylessTargetModule, "resolveKeylessTarget").mockResolvedValue( + undefined, + ); }); afterEach(() => { @@ -27,6 +34,7 @@ describe("bapi-command", () => { resolveAppContextSpy.mockRestore(); fetchApplicationSpy.mockRestore(); validateKeyPrefixSpy.mockRestore(); + resolveKeylessTargetSpy.mockRestore(); }); test("normalizes unversioned paths", () => { @@ -190,12 +198,50 @@ describe("bapi-command", () => { expect(fetchApplicationSpy).toHaveBeenCalledWith("app_123"); }); - test("falls back to CLERK_SECRET_KEY when no explicit app is provided", async () => { - process.env.CLERK_SECRET_KEY = "sk_env_123"; + test("falls back to the shared keyless resolution when no explicit app or secret key is provided", async () => { + resolveKeylessTargetSpy.mockResolvedValue({ + secretKey: "sk_env_123", + source: "CLERK_SECRET_KEY env var", + }); await expect(resolveBapiSecretKey({ instance: "dev" })).resolves.toBe("sk_env_123"); - expect(validateKeyPrefixSpy).toHaveBeenCalledWith("sk_env_123", "sk_"); + expect(resolveKeylessTargetSpy).toHaveBeenCalledWith({ instance: "dev", cwd: undefined }); + expect(resolveAppContextSpy).not.toHaveBeenCalled(); + expect(fetchApplicationSpy).not.toHaveBeenCalled(); + }); + + test("prefers an explicit app over the keyless project's own key", async () => { + resolveKeylessTargetSpy.mockResolvedValue({ + secretKey: "sk_keyless_123", + source: ".env.local", + }); + fetchApplicationSpy.mockResolvedValue({ + application_id: "app_123", + instances: [ + { + instance_id: "ins_dev", + environment_type: "development", + secret_key: "sk_test_123", + }, + ], + }); + + await expect(resolveBapiSecretKey({ app: "app_123", instance: "dev" })).resolves.toBe( + "sk_test_123", + ); + + expect(resolveKeylessTargetSpy).not.toHaveBeenCalled(); + }); + + test("prefers keyless resolution over the account-linked fallback", async () => { + resolveKeylessTargetSpy.mockResolvedValue({ + secretKey: "sk_keyless_123", + source: ".env.local", + }); + + await expect(resolveBapiSecretKey({})).resolves.toBe("sk_keyless_123"); + expect(resolveAppContextSpy).not.toHaveBeenCalled(); expect(fetchApplicationSpy).not.toHaveBeenCalled(); }); @@ -247,6 +293,16 @@ describe("bapi-command", () => { await expect(describeBapiTarget({ secretKey: "sk_test_123" })).resolves.toBeUndefined(); }); + test("describes an unclaimed keyless target without querying the account", async () => { + resolveKeylessTargetSpy.mockResolvedValue({ secretKey: "sk_test_123", source: ".env.local" }); + + await expect(describeBapiTarget({})).resolves.toBe( + "this keyless application (secret key from .env.local)", + ); + + expect(resolveAppContextSpy).not.toHaveBeenCalled(); + }); + test("throws instance-not-found when the resolved instance is missing from the application", async () => { resolveAppContextSpy.mockResolvedValue({ appId: "app_123", diff --git a/packages/cli-core/src/lib/bapi-command.ts b/packages/cli-core/src/lib/bapi-command.ts index 22f22630d..dd722d20a 100644 --- a/packages/cli-core/src/lib/bapi-command.ts +++ b/packages/cli-core/src/lib/bapi-command.ts @@ -1,5 +1,6 @@ import { resolveAppContext, resolveFetchedApplicationInstance } from "./config.ts"; import { BapiError, CliError, ERROR_CODE, throwUsageError, withApiContext } from "./errors.ts"; +import { resolveKeylessTarget } from "./keyless-target.ts"; import { log } from "./log.ts"; import { fetchApplication, validateKeyPrefix } from "./plapi.ts"; @@ -14,13 +15,29 @@ interface ResolveBapiSecretKeyOptions { app?: string; instance?: string; secretKey?: string; + cwd?: string; } export async function describeBapiTarget( options: ResolveBapiSecretKeyOptions, ): Promise { + // Mirrors resolveBapiSecretKey's precedence: an unclaimed keyless project has + // no app/instance to describe, only the key's own source. + const keyless = await resolveKeylessTarget({ + app: options.app, + instance: options.instance, + cwd: options.cwd, + }); + if (keyless) { + return `this keyless application (secret key from ${keyless.source})`; + } + try { - const ctx = await resolveAppContext({ app: options.app, instance: options.instance }); + const ctx = await resolveAppContext({ + app: options.app, + instance: options.instance, + cwd: options.cwd, + }); return `${ctx.appLabel} (${ctx.instanceLabel})`; } catch (error) { if ( @@ -58,14 +75,24 @@ export async function resolveBapiSecretKey(options: ResolveBapiSecretKeyOptions) return resolved.instance.secret_key; } - if (process.env.CLERK_SECRET_KEY) { - validateKeyPrefix(process.env.CLERK_SECRET_KEY, "sk_"); - return process.env.CLERK_SECRET_KEY; + // An unclaimed keyless application keeps its only secret key on disk (env var, + // .env.local, or the SDK's own keyless.json) — the same resolution `whoami`, + // `config`, and `env pull` already share. The shipped binary is compiled with + // --no-compile-autoload-dotenv, so without this every `users`/`api` command + // would report "no secret key" on a perfectly live keyless project the moment + // it wasn't run via `bun run` (which autoloads .env.local for us in dev). + const keyless = await resolveKeylessTarget({ instance: options.instance, cwd: options.cwd }); + if (keyless) { + return keyless.secretKey; } let ctx: Awaited>; try { - ctx = await resolveAppContext({ app: options.app, instance: options.instance }); + ctx = await resolveAppContext({ + app: options.app, + instance: options.instance, + cwd: options.cwd, + }); } catch (error) { if (error instanceof CliError && error.code === ERROR_CODE.NOT_LINKED) { throwUsageError( diff --git a/packages/cli-core/src/lib/credential-store.ts b/packages/cli-core/src/lib/credential-store.ts index d2df0b207..a8c8211e2 100644 --- a/packages/cli-core/src/lib/credential-store.ts +++ b/packages/cli-core/src/lib/credential-store.ts @@ -302,6 +302,22 @@ function isInvalidGrant(error: unknown): boolean { ); } +/** + * A 4xx from the token endpoint means this stored session will never refresh — + * `invalid_client` when the OAuth client no longer recognises us, `invalid_grant` + * when the refresh token itself is spent. Either way the user's next step is to + * log in again, so it should read as an expired session rather than as a raw + * `Token refresh failed (401): {"error":"invalid_client",…}` leaking out of the + * credential layer. + * + * 5xx and network failures are deliberately excluded: those are transient, and + * telling someone their session expired because Clerk had a bad minute would + * send them to re-authenticate for nothing. + */ +function isUnrecoverableRefreshFailure(error: unknown): boolean { + return error instanceof ApiError && error.status >= 400 && error.status < 500; +} + async function readStoredValue(): Promise { if (tokenOverride !== undefined) return tokenOverride; @@ -373,6 +389,15 @@ async function refreshStoredSession(session: OAuthSession): Promise { await deleteToken(); throw sessionExpiredError(); } + // Other 4xx refusals are just as terminal, but the stored session isn't + // provably spent the way a rotated refresh token is — so report it and + // leave the credentials alone rather than logging the user out for them. + if (isUnrecoverableRefreshFailure(error)) { + log.debug( + `credentials: refresh refused, treating session as expired (${errorMessage(error)})`, + ); + throw sessionExpiredError(); + } throw error; } diff --git a/packages/cli-core/src/lib/keyless-target.test.ts b/packages/cli-core/src/lib/keyless-target.test.ts new file mode 100644 index 000000000..9f9432f27 --- /dev/null +++ b/packages/cli-core/src/lib/keyless-target.test.ts @@ -0,0 +1,181 @@ +import { test, expect, describe, beforeEach, afterEach, spyOn } from "bun:test"; +import { mkdtemp, rm } from "node:fs/promises"; +import { join } from "node:path"; +import { tmpdir } from "node:os"; +import { useCaptureLog } from "../test/lib/stubs.ts"; + +const configModule = await import("./config.ts"); +const credentialStoreModule = await import("./credential-store.ts"); +const bapiModule = await import("./bapi.ts"); + +const { resolveKeylessTarget, resolveInstanceTarget, hasKeyPairMismatch } = + await import("./keyless-target.ts"); + +/** Encodes `.clerk.accounts.dev$` the way a real publishable key would. */ +function encodeFapiHost(host: string): string { + return `pk_test_${Buffer.from(`${host}.clerk.accounts.dev$`).toString("base64").replace(/=+$/, "")}`; +} + +describe("keyless-target", () => { + let resolveProfileSpy: ReturnType; + let hasAccountCredentialsSpy: ReturnType; + let getStoredSessionSpy: ReturnType; + let bapiRequestSpy: ReturnType; + const originalEnv = { ...process.env }; + const captured = useCaptureLog(); + let tempDir: string; + + beforeEach(async () => { + tempDir = await mkdtemp(join(tmpdir(), "clerk-keyless-target-")); + resolveProfileSpy = spyOn(configModule, "resolveProfile").mockResolvedValue(undefined); + hasAccountCredentialsSpy = spyOn( + credentialStoreModule, + "hasAccountCredentials", + ).mockResolvedValue(false); + getStoredSessionSpy = spyOn(credentialStoreModule, "getStoredSession").mockResolvedValue(null); + bapiRequestSpy = spyOn(bapiModule, "bapiRequest"); + }); + + afterEach(async () => { + process.env = { ...originalEnv }; + resolveProfileSpy.mockRestore(); + hasAccountCredentialsSpy.mockRestore(); + getStoredSessionSpy.mockRestore(); + bapiRequestSpy.mockRestore(); + await rm(tempDir, { recursive: true, force: true }); + }); + + describe("hasKeyPairMismatch", () => { + const KEYLESS = { secretKey: "sk_test_x", source: ".env.local" }; + const MATCHING_PK = encodeFapiHost("match"); + const MISMATCHED_PK = encodeFapiHost("other"); + + test("resolves false when the publishable key's host is among the secret key's own domains", async () => { + bapiRequestSpy.mockResolvedValue({ + body: { data: [{ frontend_api_url: "https://match.clerk.accounts.dev" }] }, + }); + + await expect(hasKeyPairMismatch(KEYLESS, MATCHING_PK)).resolves.toBe(false); + }); + + test("resolves true when the publishable key names a different application's host", async () => { + bapiRequestSpy.mockResolvedValue({ + body: { data: [{ frontend_api_url: "https://match.clerk.accounts.dev" }] }, + }); + + await expect(hasKeyPairMismatch(KEYLESS, MISMATCHED_PK)).resolves.toBe(true); + }); + + test("matches against any of several domains, not just the first", async () => { + bapiRequestSpy.mockResolvedValue({ + body: { + data: [ + { frontend_api_url: "https://unrelated.clerk.accounts.dev" }, + { frontend_api_url: "https://match.clerk.accounts.dev" }, + ], + }, + }); + + await expect(hasKeyPairMismatch(KEYLESS, MATCHING_PK)).resolves.toBe(false); + }); + + test("resolves false for a malformed publishable key rather than reporting a mismatch", async () => { + await expect(hasKeyPairMismatch(KEYLESS, "not_a_publishable_key")).resolves.toBe(false); + // A malformed key can't be decoded, so there was nothing to check against BAPI for. + expect(bapiRequestSpy).not.toHaveBeenCalled(); + }); + + test("resolves false when BAPI returns an unexpected shape rather than blocking on it", async () => { + bapiRequestSpy.mockResolvedValue({ body: { data: "not-an-array" } }); + + await expect(hasKeyPairMismatch(KEYLESS, MATCHING_PK)).resolves.toBe(false); + }); + + test("propagates a BAPI failure so callers can decide how strict to be", async () => { + bapiRequestSpy.mockRejectedValue(new Error("network down")); + + await expect(hasKeyPairMismatch(KEYLESS, MATCHING_PK)).rejects.toThrow("network down"); + }); + }); + + // The warning that the keyless view covers less belongs to the config + // surface, so it hangs off `resolveInstanceTarget` — the resolver itself + // stays silent for `whoami`, `open`, `env pull` and `doctor`. + describe("resolveInstanceTarget — not-linked warning", () => { + const session = (expiresAt: number) => ({ + accessToken: "at", + refreshToken: "rt", + expiresAt, + tokenType: "Bearer" as const, + }); + + beforeEach(() => { + process.env.CLERK_SECRET_KEY = "sk_test_x"; + }); + + afterEach(() => { + delete process.env.CLERK_SECRET_KEY; + delete process.env.CLERK_PLATFORM_API_KEY; + }); + + test("resolving a target on its own never warns", async () => { + hasAccountCredentialsSpy.mockResolvedValue(true); + getStoredSessionSpy.mockResolvedValue(session(Date.now() - 60_000)); + + await resolveKeylessTarget({ cwd: tempDir }); + + expect(captured.err).toBe(""); + }); + + test("no warning at all when there are no account credentials", async () => { + await resolveInstanceTarget({ cwd: tempDir }); + + expect(captured.err).toBe(""); + }); + + test("points at `clerk link` when the stored session isn't locally expired", async () => { + hasAccountCredentialsSpy.mockResolvedValue(true); + getStoredSessionSpy.mockResolvedValue(session(Date.now() + 60_000)); + + await resolveInstanceTarget({ cwd: tempDir }); + + expect(captured.err).toContain("isn't linked to an application"); + expect(captured.err).toContain("Run `clerk link`"); + expect(captured.err).not.toContain("session has expired"); + }); + + test("points at `clerk auth login` first when the stored session has locally expired", async () => { + hasAccountCredentialsSpy.mockResolvedValue(true); + getStoredSessionSpy.mockResolvedValue(session(Date.now() - 60_000)); + + await resolveInstanceTarget({ cwd: tempDir }); + + expect(captured.err).toContain("stored session has expired"); + expect(captured.err).toContain("Run `clerk auth login` to re-authenticate"); + }); + + test("uses the `clerk link` wording for a platform API key, which never locally expires", async () => { + hasAccountCredentialsSpy.mockResolvedValue(true); + getStoredSessionSpy.mockResolvedValue(null); + process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform"; + + await resolveInstanceTarget({ cwd: tempDir }); + + expect(captured.err).toContain("isn't linked to an application"); + expect(captured.err).not.toContain("session has expired"); + }); + + // `clerk link` accepts a platform API key on its own, so a leftover expired + // OAuth session alongside one must not send the user to log in again. + test("a platform API key wins over a stale stored session", async () => { + hasAccountCredentialsSpy.mockResolvedValue(true); + getStoredSessionSpy.mockResolvedValue(session(Date.now() - 60_000)); + process.env.CLERK_PLATFORM_API_KEY = "ak_test_platform"; + + await resolveInstanceTarget({ cwd: tempDir }); + + expect(captured.err).toContain("Run `clerk link`"); + expect(captured.err).not.toContain("session has expired"); + }); + }); +}); diff --git a/packages/cli-core/src/lib/keyless-target.ts b/packages/cli-core/src/lib/keyless-target.ts index fb6be9f42..fa5fd3a6b 100644 --- a/packages/cli-core/src/lib/keyless-target.ts +++ b/packages/cli-core/src/lib/keyless-target.ts @@ -8,10 +8,12 @@ */ import { join } from "node:path"; +import { bapiRequest } from "./bapi.ts"; import { resolveAppContext, resolveProfile } from "./config.ts"; -import { hasAccountCredentials } from "./credential-store.ts"; +import { getStoredSession, hasAccountCredentials, type OAuthSession } from "./credential-store.ts"; import { parseEnvFile } from "./dotenv.ts"; import { CliError, ERROR_CODE, throwUsageError } from "./errors.ts"; +import { decodePublishableKey } from "./fapi.ts"; import { detectPublishableKeyName, detectSecretKeyName } from "./framework.ts"; import { log } from "./log.ts"; @@ -125,6 +127,60 @@ export async function findLocalPublishableKey(cwd: string): Promise { + let fapiHost: string; + try { + fapiHost = decodePublishableKey(publishableKey).fapiHost; + } catch { + return false; + } + + const response = await bapiRequest({ + method: "GET", + path: "/v1/domains", + secretKey: keyless.secretKey, + }); + const domains = (response.body as { data?: unknown }).data; + if (!Array.isArray(domains)) return false; + + const matchesADomain = domains.some((domain) => { + const frontendApiUrl = (domain as { frontend_api_url?: unknown }).frontend_api_url; + if (typeof frontendApiUrl !== "string") return false; + try { + return new URL(frontendApiUrl).host === fapiHost; + } catch { + return false; + } + }); + + return !matchesADomain; +} + /** * Resolves the keyless target for a command, or `undefined` when the * account-authenticated path applies. @@ -164,17 +220,61 @@ export async function resolveKeylessTarget(options: { ); } - // Signed in but unlinked is the one case where this fallback is surprising: - // the account could reach the full configuration if the project were linked. - // Say so rather than quietly answering with the smaller view. - if (await hasAccountCredentials()) { + return target; +} + +/** + * Says so when a signed-in user is getting the smaller, keyless view of an + * instance they could be reaching in full. + * + * This lives with `resolveInstanceTarget` rather than with the resolution + * itself because it is only true of the configuration surface: `clerk link` + * would widen what `config pull` and the feature toggles can see, but it does + * nothing for `whoami`, `env pull`, `open` or `doctor`, which want the same + * keyless answer either way. A resolver that warns is also a resolver no + * diagnostic tool can call without polluting its own report. + * + * `hasAccountCredentials` only tests presence, so a stored session past its own + * recorded expiry would otherwise be pointed at `clerk link`, which can't + * succeed until the user logs in again. Checking the token's real validity + * would mean a refresh round trip on every keyless command just to word a + * warning, so this reads the expiry already cached alongside the session — no + * network call, and right in the common case of a session stale by its own + * clock rather than one the server revoked. + */ +async function warnKeylessCoversLess(source: string): Promise { + if (!(await hasAccountCredentials())) return; + + // A platform API key never expires this way and is on its own enough to link, + // so it always gets the plain wording. + if (process.env.CLERK_PLATFORM_API_KEY) { log.warn( - `This directory isn't linked to an application — using the secret key from ${target.source}, which covers fewer settings.\n` + + `This directory isn't linked to an application — using the secret key from ${source}, which covers fewer settings.\n` + "Run `clerk link` (or pass --app ) to use the full configuration.", ); + return; } - return target; + const session = await getStoredSession(); + log.warn( + session && isLocallyExpired(session) + ? `This directory isn't linked to an application, and the stored session has expired — using the secret key from ${source}, which covers fewer settings.\n` + + "Run `clerk auth login` to re-authenticate, then `clerk link` (or pass --app ) to use the full configuration." + : `This directory isn't linked to an application — using the secret key from ${source}, which covers fewer settings.\n` + + "Run `clerk link` (or pass --app ) to use the full configuration.", + ); +} + +/** + * A cheap, local-only proxy for "the account path is currently broken": + * whether a stored session's own recorded expiry has already passed. This + * can't see a session the *server* has revoked (that needs the refresh round + * trip `getValidToken` performs), only one that's stale by its own clock — + * but that's the common case, and it's the distinction the not-linked warning + * needs without paying for a network call on every keyless command. + */ +function isLocallyExpired(session: OAuthSession): boolean { + return Number.isFinite(session.expiresAt) && session.expiresAt <= Date.now(); } export async function resolveInstanceTarget(options: { @@ -184,6 +284,7 @@ export async function resolveInstanceTarget(options: { }): Promise { const keyless = await resolveKeylessTarget(options); if (keyless) { + await warnKeylessCoversLess(keyless.source); return { kind: "keyless", keyless, From 7b53796070a3b763a5a2fcf70b2d47947a706f1b Mon Sep 17 00:00:00 2001 From: Rafael Thayto Date: Thu, 30 Jul 2026 09:17:01 -0300 Subject: [PATCH 03/10] fix(keyless): address CodeRabbit findings on secret handling and error classification - demo-env.sh: bound the accountless-application curl request with connect/max timeouts, and stop echoing the raw minting response (it can contain a live secret_key when only one of the two fields fails to parse). - 09-health-and-users.tape: redact the claim token before it's recorded by piping `clerk open --print` through sed. - open/README.md: add a language to the fenced usage-error example (MD040). - bapi-command.ts: describeBapiTarget now checks options.secretKey first, mirroring resolveBapiSecretKey's actual precedence. - credential-store.ts: exclude 429 from isUnrecoverableRefreshFailure so a rate-limited token refresh isn't reported as an expired session. --- docs/keyless-demos/lib/demo-env.sh | 4 ++-- docs/keyless-demos/tapes/09-health-and-users.tape | 2 +- packages/cli-core/src/commands/open/README.md | 2 +- packages/cli-core/src/lib/bapi-command.test.ts | 8 ++++++++ packages/cli-core/src/lib/bapi-command.ts | 4 ++++ .../cli-core/src/lib/credential-store.test.ts | 15 +++++++++++++++ packages/cli-core/src/lib/credential-store.ts | 7 +++++-- 7 files changed, 36 insertions(+), 6 deletions(-) diff --git a/docs/keyless-demos/lib/demo-env.sh b/docs/keyless-demos/lib/demo-env.sh index 0812ee7d9..73deb7218 100644 --- a/docs/keyless-demos/lib/demo-env.sh +++ b/docs/keyless-demos/lib/demo-env.sh @@ -83,7 +83,7 @@ keyless_keys() { local response publishable secret attempt for attempt in 1 2 3 4 5; do - response="$(curl -sS -X POST https://api.clerk.com/v1/accountless_applications \ + response="$(curl -sS --connect-timeout 10 --max-time 30 -X POST https://api.clerk.com/v1/accountless_applications \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d "$body")" @@ -98,7 +98,7 @@ EOF return 0 fi - echo "keyless_keys: attempt $attempt failed ($response)" >&2 + echo "keyless_keys: attempt $attempt failed to return both key values" >&2 sleep $((attempt * 5)) done diff --git a/docs/keyless-demos/tapes/09-health-and-users.tape b/docs/keyless-demos/tapes/09-health-and-users.tape index 5003af861..faf958c48 100644 --- a/docs/keyless-demos/tapes/09-health-and-users.tape +++ b/docs/keyless-demos/tapes/09-health-and-users.tape @@ -28,7 +28,7 @@ Sleep 500ms Enter Sleep 8s -Type "clerk open --print" +Type `clerk open --print | sed -E 's/(token=)[^&]+/\1REDACTED/'` Sleep 500ms Enter Sleep 8s diff --git a/packages/cli-core/src/commands/open/README.md b/packages/cli-core/src/commands/open/README.md index 770618148..1d769f317 100644 --- a/packages/cli-core/src/commands/open/README.md +++ b/packages/cli-core/src/commands/open/README.md @@ -38,7 +38,7 @@ If a claim link is found, `open` opens/prints/emits **that** URL instead of a da A `subpath` cannot be honored for an unclaimed application (there is no dashboard page beyond the claim link yet), so `open users` on an unclaimed app throws instead of silently opening the claim link at the wrong URL: -``` +```text "users" isn't reachable yet — this application hasn't been claimed, so it has no dashboard pages beyond the claim link. Run `clerk open` (no subpath) to claim it, then retry the subpath once it's linked. diff --git a/packages/cli-core/src/lib/bapi-command.test.ts b/packages/cli-core/src/lib/bapi-command.test.ts index b4fa6cf25..a7fd782fb 100644 --- a/packages/cli-core/src/lib/bapi-command.test.ts +++ b/packages/cli-core/src/lib/bapi-command.test.ts @@ -303,6 +303,14 @@ describe("bapi-command", () => { expect(resolveAppContextSpy).not.toHaveBeenCalled(); }); + test("an explicit --secret-key wins over a keyless target on disk, matching resolveBapiSecretKey", async () => { + resolveKeylessTargetSpy.mockResolvedValue({ secretKey: "sk_test_disk", source: ".env.local" }); + + await expect(describeBapiTarget({ secretKey: "sk_test_explicit" })).resolves.toBeUndefined(); + + expect(resolveKeylessTargetSpy).not.toHaveBeenCalled(); + }); + test("throws instance-not-found when the resolved instance is missing from the application", async () => { resolveAppContextSpy.mockResolvedValue({ appId: "app_123", diff --git a/packages/cli-core/src/lib/bapi-command.ts b/packages/cli-core/src/lib/bapi-command.ts index dd722d20a..e44366484 100644 --- a/packages/cli-core/src/lib/bapi-command.ts +++ b/packages/cli-core/src/lib/bapi-command.ts @@ -21,6 +21,10 @@ interface ResolveBapiSecretKeyOptions { export async function describeBapiTarget( options: ResolveBapiSecretKeyOptions, ): Promise { + // An explicit --secret-key wins in resolveBapiSecretKey, so it has no + // app/instance context to describe. + if (options.secretKey) return undefined; + // Mirrors resolveBapiSecretKey's precedence: an unclaimed keyless project has // no app/instance to describe, only the key's own source. const keyless = await resolveKeylessTarget({ diff --git a/packages/cli-core/src/lib/credential-store.test.ts b/packages/cli-core/src/lib/credential-store.test.ts index 8ad503dab..b50df9270 100644 --- a/packages/cli-core/src/lib/credential-store.test.ts +++ b/packages/cli-core/src/lib/credential-store.test.ts @@ -154,6 +154,21 @@ describe("credential-store", () => { expect(await getStoredSession()).toBeNull(); }); + test("getValidToken treats a 429 refresh failure as transient, not an expired session", async () => { + const session = { + accessToken: "expired-access-token", + refreshToken: "refresh-token", + expiresAt: Date.now() - 60_000, + tokenType: "Bearer", + }; + await storeToken(session); + + mockRefreshAccessToken.mockRejectedValue(new ApiError(429, "rate_limited")); + + await expect(getValidToken()).rejects.toBeInstanceOf(ApiError); + expect(await getStoredSession()).toEqual(session); + }); + test("createOAuthSession requires a refresh token in the auth response", () => { expect(() => createOAuthSession({ diff --git a/packages/cli-core/src/lib/credential-store.ts b/packages/cli-core/src/lib/credential-store.ts index a8c8211e2..61c85efd8 100644 --- a/packages/cli-core/src/lib/credential-store.ts +++ b/packages/cli-core/src/lib/credential-store.ts @@ -312,10 +312,13 @@ function isInvalidGrant(error: unknown): boolean { * * 5xx and network failures are deliberately excluded: those are transient, and * telling someone their session expired because Clerk had a bad minute would - * send them to re-authenticate for nothing. + * send them to re-authenticate for nothing. 429 is excluded for the same + * reason — it's a rate limit, not a rejection of the refresh token. */ function isUnrecoverableRefreshFailure(error: unknown): boolean { - return error instanceof ApiError && error.status >= 400 && error.status < 500; + return ( + error instanceof ApiError && error.status >= 400 && error.status < 500 && error.status !== 429 + ); } async function readStoredValue(): Promise { From 41f73aab50428b73cada252a6b527ec63ddde4da Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Thu, 30 Jul 2026 18:04:05 -0300 Subject: [PATCH 04/10] chore(keyless): remove demo assets from the repo --- docs/keyless-demos/.gitignore | 2 - docs/keyless-demos/README.md | 80 ------------- docs/keyless-demos/lib/demo-env.sh | 107 ------------------ docs/keyless-demos/record.sh | 59 ---------- .../screenshots/01-signin-before.png | Bin 25144 -> 0 bytes .../screenshots/02-signup-before.png | Bin 29797 -> 0 bytes .../screenshots/03-orgs-enabled-after.png | Bin 30771 -> 0 bytes .../screenshots/04-orgs-disabled-after.png | Bin 133851 -> 0 bytes .../05-signup-restricted-after.png | Bin 40250 -> 0 bytes .../screenshots/06-signup-allowed-after.png | Bin 19898 -> 0 bytes docs/keyless-demos/screenshots/README.md | 56 --------- docs/keyless-demos/tapes/01-bootstrap.tape | 22 ---- docs/keyless-demos/tapes/02-read-config.tape | 30 ----- .../keyless-demos/tapes/03-change-config.tape | 39 ------- docs/keyless-demos/tapes/04-toggles.tape | 29 ----- docs/keyless-demos/tapes/05-templates.tape | 25 ---- docs/keyless-demos/tapes/06-guardrails.tape | 33 ------ docs/keyless-demos/tapes/07-agent-mode.tape | 35 ------ .../tapes/08-sso-connections.tape | 37 ------ .../tapes/09-health-and-users.tape | 35 ------ docs/keyless-demos/tapes/_common.tape | 19 ---- 21 files changed, 608 deletions(-) delete mode 100644 docs/keyless-demos/.gitignore delete mode 100644 docs/keyless-demos/README.md delete mode 100644 docs/keyless-demos/lib/demo-env.sh delete mode 100755 docs/keyless-demos/record.sh delete mode 100644 docs/keyless-demos/screenshots/01-signin-before.png delete mode 100644 docs/keyless-demos/screenshots/02-signup-before.png delete mode 100644 docs/keyless-demos/screenshots/03-orgs-enabled-after.png delete mode 100644 docs/keyless-demos/screenshots/04-orgs-disabled-after.png delete mode 100644 docs/keyless-demos/screenshots/05-signup-restricted-after.png delete mode 100644 docs/keyless-demos/screenshots/06-signup-allowed-after.png delete mode 100644 docs/keyless-demos/screenshots/README.md delete mode 100644 docs/keyless-demos/tapes/01-bootstrap.tape delete mode 100644 docs/keyless-demos/tapes/02-read-config.tape delete mode 100644 docs/keyless-demos/tapes/03-change-config.tape delete mode 100644 docs/keyless-demos/tapes/04-toggles.tape delete mode 100644 docs/keyless-demos/tapes/05-templates.tape delete mode 100644 docs/keyless-demos/tapes/06-guardrails.tape delete mode 100644 docs/keyless-demos/tapes/07-agent-mode.tape delete mode 100644 docs/keyless-demos/tapes/08-sso-connections.tape delete mode 100644 docs/keyless-demos/tapes/09-health-and-users.tape delete mode 100644 docs/keyless-demos/tapes/_common.tape diff --git a/docs/keyless-demos/.gitignore b/docs/keyless-demos/.gitignore deleted file mode 100644 index 8dc6a2d3f..000000000 --- a/docs/keyless-demos/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -bin/ -out/ diff --git a/docs/keyless-demos/README.md b/docs/keyless-demos/README.md deleted file mode 100644 index a9de1502f..000000000 --- a/docs/keyless-demos/README.md +++ /dev/null @@ -1,80 +0,0 @@ -# Keyless demos - -Recorded walkthroughs of what the CLI can do against an **unclaimed keyless -application** — a Clerk app created anonymously, with no account, no login, and -no browser round-trip. - -Each tape is a script of exactly what is shown. Re-record them with: - -```sh -./docs/keyless-demos/record.sh # every tape -./docs/keyless-demos/record.sh 03 06 # just these -``` - -Requires [VHS](https://github.com/charmbracelet/vhs) (`brew install vhs`). - -## How they run - -`clerk` on the tapes' `PATH` is a shim that runs this working tree, so a -recording always shows _this checkout_ rather than a globally installed CLI. - -It runs the TypeScript entry point rather than the `build:compile` binary on -purpose: a locally built binary is unsigned, so macOS raises a keychain -authorization dialog the first time it reads the credential store — which a -headless recording can never answer, and the tape hangs. The shim passes -`--env-file=/dev/null` so Bun's `.env` autoload stays out of the way and the CLI -resolves keys from the project's files itself, exactly as the signed release -binary does. Without it, every key would be reported as coming from an -environment variable rather than from `.env.local`. - -Every tape mints its own keyless application against the real Clerk API through -the public `POST /v1/accountless_applications` endpoint — the same one -`clerk init --keyless` uses. Nothing needs cleaning up afterwards: unclaimed -applications are throwaway by construction. - -### The `[KEYLESS-DEMO]` banner - -These demos are about a developer who has never logged in, recorded on a machine -that has. `signed_out()` in `lib/demo-env.sh` points the CLI at an empty config -directory and at an environment named `keyless-demo`. Sessions are keyed -`oauth-access-token:` in the OS keyring, so that name alone misses -every stored credential without reading, touching, or deleting any of them — and -the CLI prints its non-production banner as a result. - -It is only the credential store that is isolated. Both environment profiles hold -the real Clerk URLs: every request in every recording goes to `api.clerk.com` -against a genuine application. - -Only publishable keys and instance IDs ever reach the screen. Secret keys go -straight into `.env.local`, which no tape prints. If you add a tape, keep it -that way. - -## The tapes - -| Tape | What it shows | -| -------------------------- | ------------------------------------------------------------------------------------------- | -| `01-bootstrap.tape` | `clerk init --keyless` on a signed-out machine, then `clerk whoami` naming the new instance | -| `02-read-config.tape` | `clerk config pull`, whole and by `--keys`, authenticated only by the local secret key | -| `03-change-config.tape` | `clerk config patch` — dry run, apply, read back, multi-group payload | -| `04-toggles.tape` | `clerk enable orgs` / `clerk disable orgs`, including the idempotent second run | -| `05-templates.tape` | `clerk init --template b2b-saas`, which returns an app with organizations already on | -| `06-guardrails.tape` | The refusals: `config put`, an account-only config key, `--instance`, `enable billing` | -| `07-agent-mode.tape` | The same application driven by `--mode agent`, where every answer is JSON | -| `08-sso-connections.tape` | Adding and removing an enterprise SSO connection, which needs no account | -| `09-health-and-users.tape` | `clerk doctor`, `clerk users list` and `clerk open --print` on a keyless project | - -## Layout - -``` -docs/keyless-demos/ -├── record.sh # write the `clerk` shim, then run the tapes -├── lib/demo-env.sh # sourced by each tape inside a Hide block -├── tapes/ # one tape per workflow; _common.tape holds the shared look -├── bin/ # the `clerk` shim under test (gitignored) -└── out/ # rendered gif + mp4 (gitignored) -``` - -The throwaway projects the tapes build go under `$TMPDIR/clerk-keyless-demos`, -outside the repository. Inside a dirty git worktree, `clerk init` opens by -telling you to commit first — correct advice, and pure noise in a recording -about something else. diff --git a/docs/keyless-demos/lib/demo-env.sh b/docs/keyless-demos/lib/demo-env.sh deleted file mode 100644 index 73deb7218..000000000 --- a/docs/keyless-demos/lib/demo-env.sh +++ /dev/null @@ -1,107 +0,0 @@ -# Helpers the demo tapes source inside a `Hide` block, so the recording shows -# the Clerk commands rather than the scaffolding around them. -# -# Sourced, never executed: `new_project` has to change the caller's directory. - -# A quiet prompt with no history file, so recordings are reproducible. -quiet_shell() { - export PS1='$ ' - export PROMPT_COMMAND= - export HISTFILE=/dev/null - # Keep the CLI's own update check out of the recording. - export CLERK_NO_UPDATE_CHECK=1 -} - -# These demos are about a developer who has never logged in, but they get -# recorded on a machine that has. Point the CLI at an empty config directory and -# at an environment name nothing is stored under, so it genuinely finds no -# account — rather than faking one with a mock API. -# -# Credentials are keyed `oauth-access-token:` in the OS keyring, so -# naming an environment other than `production` is enough to miss every stored -# session without touching, reading, or deleting any of them. Both profiles hold -# the real Clerk URLs: every request in these recordings goes to the live API. -signed_out() { - local dir="${1:?config dir required}" - mkdir -p "$dir" - printf '{"environment":"keyless-demo"}\n' > "$dir/config.json" - export CLERK_CONFIG_DIR="$dir" - - local profile='{ - "oauthClientId": "ins_1lyWDZiobr600AKUeQDoSlrEmoM", - "oauthBaseUrl": "https://clerk.clerk.com", - "platformApiUrl": "https://api.clerk.com", - "backendApiUrl": "https://api.clerk.dev", - "dashboardUrl": "https://dashboard.clerk.com" - }' - printf '{"production": %s, "keyless-demo": %s}\n' "$profile" "$profile" > .env-profiles.json -} - -# Fresh Next.js-shaped project at work/, and cd into it. The `next` -# dependency is what drives the CLI's framework detection and the -# NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY naming. -# -# Built outside the repository on purpose. A project inside a dirty git worktree -# makes `clerk init` open with "You have uncommitted changes", which is correct -# advice and pure noise in a recording about something else. -new_project() { - local name="${1:?project name required}" - local root="${TMPDIR:-/tmp}/clerk-keyless-demos" - - rm -rf "$root/$name" - mkdir -p "$root/$name" - cd "$root/$name" || return 1 - - cat > package.json <<'JSON' -{ - "name": "acme-dashboard", - "private": true, - "dependencies": { - "next": "^15.0.0", - "react": "^19.0.0", - "@clerk/nextjs": "^6.0.0" - } -} -JSON - - signed_out "$root/.config-$name" -} - -# Mint an unclaimed keyless application straight from Clerk's public endpoint -# and drop its keys where the CLI will find them. Demos that are *about* -# configuring a keyless app start here instead of replaying `clerk init`. -# -# The endpoint rate-limits, and a half-written .env.local is worse than no -# recording at all — it makes the CLI fall through to the account path and the -# demo silently shows the wrong thing. So: retry, then fail loudly. -# -# $1 — optional application template (b2b-saas, b2c-saas, native, waitlist) -keyless_keys() { - local template="${1:-}" - local body="source=cli" - [ -n "$template" ] && body="$body&template=$template" - - local response publishable secret attempt - for attempt in 1 2 3 4 5; do - response="$(curl -sS --connect-timeout 10 --max-time 30 -X POST https://api.clerk.com/v1/accountless_applications \ - -H 'Content-Type: application/x-www-form-urlencoded' \ - -d "$body")" - - publishable="$(printf '%s' "$response" | grep -o '"publishable_key":"[^"]*"' | cut -d'"' -f4)" - secret="$(printf '%s' "$response" | grep -o '"secret_key":"[^"]*"' | cut -d'"' -f4)" - - if [ -n "$publishable" ] && [ -n "$secret" ]; then - cat > .env.local <&2 - sleep $((attempt * 5)) - done - - echo "keyless_keys: could not mint a keyless application; refusing to record" >&2 - return 1 -} diff --git a/docs/keyless-demos/record.sh b/docs/keyless-demos/record.sh deleted file mode 100755 index a7594696d..000000000 --- a/docs/keyless-demos/record.sh +++ /dev/null @@ -1,59 +0,0 @@ -#!/usr/bin/env bash -# -# Re-record the keyless demos. -# -# ./docs/keyless-demos/record.sh # every tape -# ./docs/keyless-demos/record.sh 03 06 # only tapes whose name starts with 03 or 06 -# -# Each run compiles the CLI from the current working tree and puts that binary -# first on PATH, so what you see recorded is this checkout — not whatever -# `clerk` happens to be installed globally. -# -# Every tape mints its own unclaimed keyless application against the real Clerk -# API. No account, no login, nothing to clean up: unclaimed applications expire -# on their own. Only publishable keys and instance IDs ever reach the screen. - -set -euo pipefail - -here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -repo="$(cd "$here/../.." && pwd)" - -command -v vhs >/dev/null || { echo "vhs is not installed: brew install vhs" >&2; exit 1; } - -mkdir -p "$here/bin" "$here/out" "$here/work" - -# `clerk` on the tapes' PATH is this working tree, so a recording always shows -# the current checkout rather than a globally installed CLI. -# -# It runs the TypeScript entry point rather than `build:compile`'s binary on -# purpose. A locally built binary is unsigned, so macOS puts up a keychain -# authorization dialog the first time it reads the credential store — which a -# headless recording can never answer, and the tape hangs. `--env-file=/dev/null` -# turns off Bun's .env autoload so the CLI resolves keys from the project's files -# itself, exactly as the signed release binary does (it is compiled with -# --no-compile-autoload-dotenv). Without it the CLI would report every key as -# coming from an environment variable. -cat > "$here/bin/clerk" <&2; exit 1; } - -for tape in "${tapes[@]}"; do - echo "==> recording $(basename "$tape")" - (cd "$here/tapes" && vhs "$(basename "$tape")") -done - -echo "==> done, output in $here/out" -ls -la "$here/out" diff --git a/docs/keyless-demos/screenshots/01-signin-before.png b/docs/keyless-demos/screenshots/01-signin-before.png deleted file mode 100644 index 1df48965116d3c4746a6667505cb6484599411c6..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 25144 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 B4N?FA diff --git a/docs/keyless-demos/screenshots/02-signup-before.png b/docs/keyless-demos/screenshots/02-signup-before.png deleted file mode 100644 index 054dbf60c7804ce5ef1864e81c71a3b63e81a39a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29797 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 Cy)tM3 diff --git a/docs/keyless-demos/screenshots/03-orgs-enabled-after.png b/docs/keyless-demos/screenshots/03-orgs-enabled-after.png deleted file mode 100644 index 3605f4865df6ef75bfbd4cb814b288bb04f751b5..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 30771 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 diff --git a/docs/keyless-demos/screenshots/04-orgs-disabled-after.png b/docs/keyless-demos/screenshots/04-orgs-disabled-after.png deleted file mode 100644 index 93a907ff6f7fffd3f45f5e719652b8651cfd4d95..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 133851 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

_}1*_+@+pRJ_nqeZ?Z$Zf}lu21b~gkP6#S%!Bf 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

QOJ4o!V}w-vUWd4bR2N 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 kdl!zO@X>_>F(~5jzxFu$KQYN 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 Q5%?n~tt^!<@%Y961u!c1z5oCK diff --git a/docs/keyless-demos/screenshots/06-signup-allowed-after.png b/docs/keyless-demos/screenshots/06-signup-allowed-after.png deleted file mode 100644 index 6e65199329a3c1da3c5a8c2e0f6479553549ce4d..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 19898 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/v1/environment?__clerk_api_version=2025-04-10&_clerk_js_version=5.100.0" -``` - -`clerk enable orgs` moves `organization_settings.enabled` there from `false` to -`true`, which is why the sign-in step appears. - -Restrictions are different, and the difference is worth knowing before you go -looking for it in the wrong place. Enabling the allowlist leaves -`user_settings.sign_up.mode` reading `public` — that field describes what the -sign-up form should _render_, and the form still renders. The rule is enforced -when the sign-up is submitted. So the only honest proof for a restriction is to -attempt the operation and be refused, which is what `05` and `06` capture. - -## Re-creating these - -They were captured against a throwaway application whose keys are long gone; -the URLs in them will not resolve. To reproduce, mint your own and drive the -portal at the `accounts_portal_url` from: - -```sh -clerk api /domains # works with only an instance secret key -``` - -Nothing needs cleaning up. Unclaimed applications are throwaway by construction. diff --git a/docs/keyless-demos/tapes/01-bootstrap.tape b/docs/keyless-demos/tapes/01-bootstrap.tape deleted file mode 100644 index 191bdfd45..000000000 --- a/docs/keyless-demos/tapes/01-bootstrap.tape +++ /dev/null @@ -1,22 +0,0 @@ -# Bootstrap a Clerk application with no account, no browser, no login. - -Output ../out/01-bootstrap.gif -Output ../out/01-bootstrap.mp4 - -Source "_common.tape" - -Hide -Type "source ../lib/demo-env.sh && quiet_shell && new_project bootstrap && clear" Enter -Show - -Sleep 1s -Type "clerk init --framework next --keyless --yes --no-skills" -Sleep 700ms -Enter -Sleep 30s - -Type "clerk whoami" -Sleep 500ms -Enter -Sleep 8s -Sleep 2s diff --git a/docs/keyless-demos/tapes/02-read-config.tape b/docs/keyless-demos/tapes/02-read-config.tape deleted file mode 100644 index 6859c44fe..000000000 --- a/docs/keyless-demos/tapes/02-read-config.tape +++ /dev/null @@ -1,30 +0,0 @@ -# Read the instance configuration of an unclaimed keyless application. -# The only credential in play is the secret key already sitting in .env.local. - -Output ../out/02-read-config.gif -Output ../out/02-read-config.mp4 - -Source "_common.tape" - -Hide -Type "source ../lib/demo-env.sh && quiet_shell && new_project read-config && keyless_keys && clear" Enter -Show - -Sleep 1s -Type "clerk config pull --keys protect" -Sleep 500ms -Enter -Sleep 8s - -Type "# every readable group at once" -Enter -Type "clerk config pull | jq 'keys'" -Sleep 500ms -Enter -Sleep 8s - -Type "clerk config pull --keys organization_settings | jq '.organization_settings | {enabled, max_allowed_memberships, domains_enabled}'" -Sleep 500ms -Enter -Sleep 9s -Sleep 2s diff --git a/docs/keyless-demos/tapes/03-change-config.tape b/docs/keyless-demos/tapes/03-change-config.tape deleted file mode 100644 index 9905804d2..000000000 --- a/docs/keyless-demos/tapes/03-change-config.tape +++ /dev/null @@ -1,39 +0,0 @@ -# Change the configuration of an unclaimed keyless application. -# Preview first, then apply, then read it back from the API. - -Output ../out/03-change-config.gif -Output ../out/03-change-config.mp4 - -Source "_common.tape" - -Hide -Type "source ../lib/demo-env.sh && quiet_shell && new_project change-config && keyless_keys && clear" Enter -Show - -Sleep 1s -Type "# --dry-run diffs locally and sends nothing" -Enter -Type `clerk config patch --json '{"instance":{"support_email":"help@acme.dev"}}' --dry-run` -Sleep 700ms -Enter -Sleep 8s - -Type `clerk config patch --json '{"instance":{"support_email":"help@acme.dev"}}' --yes` -Sleep 700ms -Enter -Sleep 9s - -Type "# read it back — this is the API talking, not a local echo" -Enter -Type "clerk config pull --keys instance" -Sleep 500ms -Enter -Sleep 8s - -Type "# several groups in one payload" -Enter -Type `clerk config patch --json '{"organization_settings":{"max_allowed_memberships":25},"protect":{"rules_enabled":true}}' --yes` -Sleep 700ms -Enter -Sleep 10s -Sleep 2s diff --git a/docs/keyless-demos/tapes/04-toggles.tape b/docs/keyless-demos/tapes/04-toggles.tape deleted file mode 100644 index d0f6222bc..000000000 --- a/docs/keyless-demos/tapes/04-toggles.tape +++ /dev/null @@ -1,29 +0,0 @@ -# Turn features on and off on an unclaimed keyless application. - -Output ../out/04-toggles.gif -Output ../out/04-toggles.mp4 - -Source "_common.tape" - -Hide -Type "source ../lib/demo-env.sh && quiet_shell && new_project toggles && keyless_keys && clear" Enter -Show - -Sleep 1s -Type "clerk enable orgs" -Sleep 500ms -Enter -Sleep 8s - -Type "# running it again is a no-op, not an error" -Enter -Type "clerk enable orgs" -Sleep 500ms -Enter -Sleep 7s - -Type "clerk disable orgs" -Sleep 500ms -Enter -Sleep 8s -Sleep 2s diff --git a/docs/keyless-demos/tapes/05-templates.tape b/docs/keyless-demos/tapes/05-templates.tape deleted file mode 100644 index ba274b172..000000000 --- a/docs/keyless-demos/tapes/05-templates.tape +++ /dev/null @@ -1,25 +0,0 @@ -# Pre-configure the keyless application at creation time with --template. -# A b2b-saas app comes back with organizations already switched on. - -Output ../out/05-templates.gif -Output ../out/05-templates.mp4 - -Source "_common.tape" - -Hide -Type "source ../lib/demo-env.sh && quiet_shell && new_project templates && clear" Enter -Show - -Sleep 1s -Type "clerk init --framework next --keyless --template b2b-saas --yes --no-skills" -Sleep 700ms -Enter -Sleep 32s - -Type "# organizations were enabled before the first key was ever used" -Enter -Type "clerk config pull --keys organization_settings" -Sleep 500ms -Enter -Sleep 9s -Sleep 2s diff --git a/docs/keyless-demos/tapes/06-guardrails.tape b/docs/keyless-demos/tapes/06-guardrails.tape deleted file mode 100644 index f7549c17f..000000000 --- a/docs/keyless-demos/tapes/06-guardrails.tape +++ /dev/null @@ -1,33 +0,0 @@ -# The honest edges. Everything keyless mode cannot do says so plainly, -# names the reason, and tells you the one command that lifts the limit. - -Output ../out/06-guardrails.gif -Output ../out/06-guardrails.mp4 - -Source "_common.tape" - -Hide -Type "source ../lib/demo-env.sh && quiet_shell && new_project guardrails && keyless_keys && clear" Enter -Show - -Sleep 1s -Type `clerk config put --json '{"instance":{}}' --yes` -Sleep 500ms -Enter -Sleep 6s - -Type `clerk config patch --json '{"session":{"lifetime":3600}}' --yes` -Sleep 500ms -Enter -Sleep 7s - -Type `clerk config patch --instance prod --json '{"instance":{}}' --yes` -Sleep 500ms -Enter -Sleep 6s - -Type "clerk enable billing" -Sleep 500ms -Enter -Sleep 6s -Sleep 2s diff --git a/docs/keyless-demos/tapes/07-agent-mode.tape b/docs/keyless-demos/tapes/07-agent-mode.tape deleted file mode 100644 index a05691e36..000000000 --- a/docs/keyless-demos/tapes/07-agent-mode.tape +++ /dev/null @@ -1,35 +0,0 @@ -# The same keyless application, driven the way an AI agent drives it: -# `--mode agent` turns every answer into JSON on stdout. - -Output ../out/07-agent-mode.gif -Output ../out/07-agent-mode.mp4 - -Source "_common.tape" - -Hide -Type "source ../lib/demo-env.sh && quiet_shell && new_project agent-mode && keyless_keys && clear" Enter -Show - -Sleep 1s -Type "clerk --mode agent whoami" -Sleep 500ms -Enter -Sleep 7s - -Type "clerk --mode agent config pull --keys organization_settings | jq '.organization_settings.enabled'" -Sleep 500ms -Enter -Sleep 7s - -Type "clerk --mode agent enable orgs" -Sleep 500ms -Enter -Sleep 8s - -Type "# and the failures are JSON too, so an agent can branch on them" -Enter -Type `clerk --mode agent config patch --json '{"nope":{}}'` -Sleep 500ms -Enter -Sleep 7s -Sleep 2s diff --git a/docs/keyless-demos/tapes/08-sso-connections.tape b/docs/keyless-demos/tapes/08-sso-connections.tape deleted file mode 100644 index 796fb5320..000000000 --- a/docs/keyless-demos/tapes/08-sso-connections.tape +++ /dev/null @@ -1,37 +0,0 @@ -# Enterprise SSO on an unclaimed keyless application — add one, then remove it. -# -# Social providers (Google, GitHub) live only in the account-level config -# document, so they genuinely need a claimed app. Enterprise SSO connections do -# not: they are their own Backend API resource, and the instance's own secret -# key reaches them. - -Output ../out/08-sso-connections.gif -Output ../out/08-sso-connections.mp4 - -Source "_common.tape" - -Hide -Type "source ../lib/demo-env.sh && quiet_shell && new_project sso && keyless_keys && clear" Enter -Show - -Sleep 1s -Type "clerk api ls enterprise" -Sleep 500ms -Enter -Sleep 7s - -Type `clerk api /enterprise_connections -X POST --yes -d '{"name":"Acme Okta","domains":["acme.test"],"provider":"saml_custom"}' | jq '{id, name, provider, domains}'` -Sleep 700ms -Enter -Sleep 10s - -Type "ID=$(clerk api /enterprise_connections | jq -r '.data[0].id') && echo $ID" -Sleep 500ms -Enter -Sleep 7s - -Type "clerk api /enterprise_connections/$ID -X DELETE --yes" -Sleep 500ms -Enter -Sleep 8s -Sleep 2s diff --git a/docs/keyless-demos/tapes/09-health-and-users.tape b/docs/keyless-demos/tapes/09-health-and-users.tape deleted file mode 100644 index faf958c48..000000000 --- a/docs/keyless-demos/tapes/09-health-and-users.tape +++ /dev/null @@ -1,35 +0,0 @@ -# Everything else a keyless project should be able to do: check its own health, -# reach its own users, and find its way to the dashboard. -# -# Each of these used to fail on a perfectly working keyless application. - -Output ../out/09-health-and-users.gif -Output ../out/09-health-and-users.mp4 - -Source "_common.tape" - -Hide -# Bootstrapped through `clerk init` rather than by dropping keys in, so the -# project has the claim token init leaves behind — which is what `open` needs. -Type "source ../lib/demo-env.sh && quiet_shell && new_project health" Enter -Type "clerk init --framework next --keyless --yes --no-skills" Enter -Sleep 40s -Type "clear" Enter -Show - -Sleep 1s -Type "clerk doctor" -Sleep 500ms -Enter -Sleep 14s - -Type "clerk users list" -Sleep 500ms -Enter -Sleep 8s - -Type `clerk open --print | sed -E 's/(token=)[^&]+/\1REDACTED/'` -Sleep 500ms -Enter -Sleep 8s -Sleep 2s diff --git a/docs/keyless-demos/tapes/_common.tape b/docs/keyless-demos/tapes/_common.tape deleted file mode 100644 index 25845a4c7..000000000 --- a/docs/keyless-demos/tapes/_common.tape +++ /dev/null @@ -1,19 +0,0 @@ -# Shared look and feel for every keyless demo. -# -# Sourced by each tape with `Source _common.tape`. Recording settings only — -# no commands — so a tape stays a readable script of what is being shown. - -Require clerk - -Set Shell bash -Set FontSize 16 -Set Width 1400 -Set Height 860 -Set Padding 28 -Set Margin 24 -Set MarginFill "#1b1a26" -Set BorderRadius 12 -Set WindowBar Colorful -Set Framerate 12 -Set Theme "Catppuccin Mocha" -Set TypingSpeed 28ms From 06e97e415775421247467f5d5e834f0661eff7ee Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Thu, 30 Jul 2026 18:20:02 -0300 Subject: [PATCH 05/10] fix(keyless): address CodeRabbit findings on doctor side effects, claim-url trust, and env-file key priority --- .../cli-core/src/commands/doctor/checks.ts | 9 ++-- .../cli-core/src/commands/doctor/context.ts | 11 +++++ .../src/commands/doctor/doctor.test.ts | 2 + .../cli-core/src/commands/doctor/types.ts | 7 +++ packages/cli-core/src/commands/env/README.md | 31 ++++++++---- packages/cli-core/src/commands/init/README.md | 4 +- .../src/commands/open/keyless-claim.test.ts | 38 ++++++++++++++ .../src/commands/open/keyless-claim.ts | 23 ++++++++- .../cli-core/src/lib/keyless-target.test.ts | 49 ++++++++++++++++++- packages/cli-core/src/lib/keyless-target.ts | 15 ++++-- packages/cli-core/src/lib/keyless.test.ts | 15 ++++++ packages/cli-core/src/lib/keyless.ts | 15 ++++++ 12 files changed, 195 insertions(+), 24 deletions(-) diff --git a/packages/cli-core/src/commands/doctor/checks.ts b/packages/cli-core/src/commands/doctor/checks.ts index 23bba7a8b..56be15ea7 100644 --- a/packages/cli-core/src/commands/doctor/checks.ts +++ b/packages/cli-core/src/commands/doctor/checks.ts @@ -7,7 +7,6 @@ import { detectPublishableKeyName, detectSecretKeyName } from "../../lib/framewo import { parseEnvFile } from "../../lib/dotenv.ts"; import { hasAccountCredentials } from "../../lib/credential-store.ts"; import type { KeylessTarget } from "../../lib/keyless-target.ts"; -import { readKeylessBreadcrumb } from "../../lib/keyless.ts"; import { getCurrentVersion, getUpdateChannel, @@ -87,8 +86,8 @@ function keylessLabel(keyless: KeylessTarget, instance: KeylessInstanceInfo | nu * renders under `--verbose`, and guidance nobody sees by default isn't * guidance. */ -async function claimHint(): Promise { - return (await readKeylessBreadcrumb(process.cwd())) +async function claimHint(ctx: DoctorContext): Promise { + return (await ctx.hasClaimBreadcrumb()) ? "Run `clerk auth login` to claim it." : "Claim it from the Clerk Dashboard — `clerk auth login` only claims applications `clerk init` created."; } @@ -104,7 +103,7 @@ export async function checkLoggedIn(ctx: DoctorContext): Promise { if (keyless) { const instance = await ctx.getKeylessInstance(); return check.pass( - `Not logged in — running on the unclaimed keyless application ${keylessLabel(keyless, instance)}. ${await claimHint()}`, + `Not logged in — running on the unclaimed keyless application ${keylessLabel(keyless, instance)}. ${await claimHint(ctx)}`, ); } @@ -216,7 +215,7 @@ export async function checkProjectLinked(ctx: DoctorContext): Promise | undefined; let keylessPromise: Promise | undefined; let keylessInstancePromise: Promise | undefined; + let claimBreadcrumbPromise: Promise | undefined; const ctx: DoctorContext = { getToken() { @@ -84,6 +86,15 @@ export function createDoctorContext(): DoctorContext { return keylessInstancePromise; }, + hasClaimBreadcrumb() { + if (!claimBreadcrumbPromise) { + // peek, not read: readKeylessBreadcrumb clears a malformed file as a + // side effect, and doctor must leave the project exactly as found. + claimBreadcrumbPromise = peekKeylessBreadcrumb(process.cwd()).then(Boolean); + } + return claimBreadcrumbPromise; + }, + fixes: { login: () => ({ label: "Log in with clerk auth login", diff --git a/packages/cli-core/src/commands/doctor/doctor.test.ts b/packages/cli-core/src/commands/doctor/doctor.test.ts index a6e110170..0d51bd140 100644 --- a/packages/cli-core/src/commands/doctor/doctor.test.ts +++ b/packages/cli-core/src/commands/doctor/doctor.test.ts @@ -107,6 +107,7 @@ function createMockContext( applicationError?: Error; keylessTarget?: KeylessTarget; keylessInstance?: KeylessInstanceInfo | null; + claimBreadcrumb?: boolean; } = {}, ): DoctorContext { return { @@ -122,6 +123,7 @@ function createMockContext( }, getKeylessTarget: async () => overrides.keylessTarget, getKeylessInstance: async () => overrides.keylessInstance ?? null, + hasClaimBreadcrumb: async () => overrides.claimBreadcrumb ?? false, fixes: { login: noopFix, link: noopFix, diff --git a/packages/cli-core/src/commands/doctor/types.ts b/packages/cli-core/src/commands/doctor/types.ts index 68bfbcf90..4b13db359 100644 --- a/packages/cli-core/src/commands/doctor/types.ts +++ b/packages/cli-core/src/commands/doctor/types.ts @@ -39,6 +39,13 @@ export interface DoctorContext { getKeylessTarget(): Promise; /** Best-effort identity of the keyless instance, for naming it in check output. */ getKeylessInstance(): Promise; + /** + * Whether a `clerk init` claim breadcrumb is present, read once and without + * side effects — `readKeylessBreadcrumb` clears a malformed file as it goes, + * which a diagnostic command must not do, and two checks reading the disk + * independently could otherwise print contradictory claim hints. + */ + hasClaimBreadcrumb(): Promise; fixes: { login: () => FixAction; link: () => FixAction; diff --git a/packages/cli-core/src/commands/env/README.md b/packages/cli-core/src/commands/env/README.md index a0b0bbb5b..af3a08183 100644 --- a/packages/cli-core/src/commands/env/README.md +++ b/packages/cli-core/src/commands/env/README.md @@ -31,19 +31,30 @@ sequenceDiagram Note over CLI: clerk env pull [--app app_123] [--instance dev|prod] [--file .env] - alt --app flag provided + alt --app flag provided or project linked + alt --app flag provided + CLI->>API: GET /v1/platform/applications/{appId} + API-->>CLI: { instances } + else Resolve project profile + CLI->>FS: Read CLI config file + FS-->>CLI: { appId, instances } + end + + %% Fetch application with keys CLI->>API: GET /v1/platform/applications/{appId} - API-->>CLI: { instances } - else Resolve project profile - CLI->>FS: Read CLI config file - FS-->>CLI: { appId, instances } + API-->>CLI: { instances: [{ instance_id, publishable_key, secret_key }] } + CLI->>CLI: Find matching instance by instance_id + else Unclaimed keyless application (no --app, not linked) + CLI->>FS: Find local keys (env vars, .env/.env.local, .clerk/.tmp/keyless.json) + FS-->>CLI: { secret_key, publishable_key? } + opt Publishable key found locally + CLI->>API: GET /v1/domains (Backend API, secret key auth) + API-->>CLI: { frontend_api_url } + CLI->>CLI: Compare against host decoded from publishable key + Note over CLI: Mismatch → error, nothing written + end end - %% Fetch application with keys - CLI->>API: GET /v1/platform/applications/{appId} - API-->>CLI: { instances: [{ instance_id, publishable_key, secret_key }] } - CLI->>CLI: Find matching instance by instance_id - %% Detect framework CLI->>FS: Read package.json FS-->>CLI: { dependencies } diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index c82bbeef0..c2312de3f 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -77,7 +77,7 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful 12. Scans for issues: hardcoded keys, leftover auth-library imports, stale API calls 13. Prints a summary of created, modified, and skipped files with recommendations 14. **Authenticated mode**: pulls development instance API keys via `clerk env pull` -15. **Unauthenticated mode**: mints a keyless application and prints instructions for development without API keys and how to connect a Clerk account later — unless an unclaimed keyless app already exists for this project (see [Re-running init on an already-keyless project](#re-running-init-on-an-already-keyless-project)), in which case the existing keys are kept and reported instead +15. **Keyless mode** (unauthenticated runs whose resolved strategy in step 2 is keyless — an unauthenticated human-mode rerun on an existing project resolves to the authenticated flow instead): mints a keyless application and prints instructions for development without API keys and how to connect a Clerk account later — unless an unclaimed keyless app already exists for this project (see [Re-running init on an already-keyless project](#re-running-init-on-an-already-keyless-project)), in which case the existing keys are kept and reported instead 16. Optionally installs Clerk agent skills (cli + core + features, plus a framework-specific skill) via the project's package runner (see [Agent skills install](#agent-skills-install)) ## Framework Detection @@ -306,7 +306,7 @@ In keyless mode, after calling `POST /v1/accountless_applications`, `clerk init` ### Re-running init on an already-keyless project -The breadcrumb is also what protects an unclaimed keyless app from being orphaned by a later `clerk init` run. As long as `.clerk/keyless.json` is present, the application it points at hasn't been claimed yet — its claim token, and anything configured on or created in that app, only exist as long as the breadcrumb and the env keys pointing at it survive. So whenever init resolves to keyless mode and finds an existing breadcrumb, it does **not** silently mint a replacement application and overwrite the env keys and breadcrumb with the new one's: +The breadcrumb is also what protects an unclaimed keyless app from being orphaned by a later `clerk init` run. As long as `.clerk/keyless.json` is present, the application it points at hasn't been claimed yet. The application and everything configured on it keep existing server-side either way — what the breadcrumb and env keys hold is the only local way to claim or reach it, so overwriting them can strand an application that still has configuration or users on it. So whenever init resolves to keyless mode and finds an existing breadcrumb, it does **not** silently mint a replacement application and overwrite the env keys and breadcrumb with the new one's: - **Human mode** (no `-y`): prompts `This project already has an unclaimed keyless application (created ). Replace it with a new one?`, defaulting to **no**. Declining keeps the existing keys and breadcrumb untouched. - **Human mode with `-y`, and all agent-mode runs**: never prompt, and default to the same safe answer — **keep the existing application**. `-y` and agent mode both mean "skip confirmations", not "consent to destroying an app that might already have configuration or users on it". diff --git a/packages/cli-core/src/commands/open/keyless-claim.test.ts b/packages/cli-core/src/commands/open/keyless-claim.test.ts index 549b7929e..40a1edd00 100644 --- a/packages/cli-core/src/commands/open/keyless-claim.test.ts +++ b/packages/cli-core/src/commands/open/keyless-claim.test.ts @@ -81,6 +81,44 @@ describe("findKeylessClaimUrl", () => { expect(destination?.source).toBe(".clerk/keyless.json"); }); + test("ignores a non-https claimUrl and falls back to the breadcrumb", async () => { + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", ".tmp", "keyless.json"), + JSON.stringify({ claimUrl: "file:///etc/passwd" }), + ); + await writeFile( + join(projectDir, ".clerk", "keyless.json"), + JSON.stringify({ claimToken: "cli-token", createdAt: new Date().toISOString() }), + ); + + const destination = await findKeylessClaimUrl(projectDir); + expect(destination?.source).toBe(".clerk/keyless.json"); + }); + + test("ignores a claimUrl that doesn't parse as a URL at all", async () => { + await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", ".tmp", "keyless.json"), + JSON.stringify({ claimUrl: 'not-a-url" --evil' }), + ); + + expect(await findKeylessClaimUrl(projectDir)).toBeUndefined(); + }); + + test("URL-encodes the breadcrumb claim token", async () => { + await mkdir(join(projectDir, ".clerk"), { recursive: true }); + await writeFile( + join(projectDir, ".clerk", "keyless.json"), + JSON.stringify({ claimToken: "cli token&extra=1", createdAt: new Date().toISOString() }), + ); + + const destination = await findKeylessClaimUrl(projectDir); + expect(destination?.url).toBe( + "https://dashboard.clerk.com/apps/claim?token=cli%20token%26extra%3D1", + ); + }); + test("ignores a malformed SDK keyless file and falls back to the breadcrumb", async () => { await mkdir(join(projectDir, ".clerk", ".tmp"), { recursive: true }); await writeFile(join(projectDir, ".clerk", ".tmp", "keyless.json"), "{ not json"); diff --git a/packages/cli-core/src/commands/open/keyless-claim.ts b/packages/cli-core/src/commands/open/keyless-claim.ts index 609ee9e83..e363ab095 100644 --- a/packages/cli-core/src/commands/open/keyless-claim.ts +++ b/packages/cli-core/src/commands/open/keyless-claim.ts @@ -40,13 +40,32 @@ async function readSdkClaimUrl(cwd: string): Promise { try { const parsed = (await file.json()) as { claimUrl?: unknown }; - return typeof parsed.claimUrl === "string" ? parsed.claimUrl : undefined; + const claimUrl = typeof parsed.claimUrl === "string" ? parsed.claimUrl : undefined; + if (claimUrl && !isHttpsUrl(claimUrl)) { + log.debug(`open: ignoring non-https claimUrl in ${SDK_KEYLESS_FILE.join("/")}`); + return undefined; + } + return claimUrl; } catch { // A half-written file during an SDK refresh isn't worth failing over. return undefined; } } +/** + * The SDK's file is written by another process, so its URL is untrusted input. + * Anything but well-formed https is dropped rather than handed to the browser + * launcher — which on Windows goes through `cmd /c start "" ""`, where a + * `file:`/`javascript:` scheme or an embedded quote stops being just a URL. + */ +function isHttpsUrl(value: string): boolean { + try { + return new URL(value).protocol === "https:"; + } catch { + return false; + } +} + /** * Finds the claim link for an unclaimed keyless application, checking both * places one can turn up depending on how keyless mode was entered: @@ -72,7 +91,7 @@ export async function findKeylessClaimUrl( if (breadcrumb) { const host = getDashboardUrl().replace(/\/$/, ""); return { - url: `${host}/apps/claim?token=${breadcrumb.claimToken}`, + url: `${host}/apps/claim?token=${encodeURIComponent(breadcrumb.claimToken)}`, source: ".clerk/keyless.json", }; } diff --git a/packages/cli-core/src/lib/keyless-target.test.ts b/packages/cli-core/src/lib/keyless-target.test.ts index 9f9432f27..cc422f338 100644 --- a/packages/cli-core/src/lib/keyless-target.test.ts +++ b/packages/cli-core/src/lib/keyless-target.test.ts @@ -1,5 +1,5 @@ import { test, expect, describe, beforeEach, afterEach, spyOn } from "bun:test"; -import { mkdtemp, rm } from "node:fs/promises"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; import { useCaptureLog } from "../test/lib/stubs.ts"; @@ -8,7 +8,7 @@ const configModule = await import("./config.ts"); const credentialStoreModule = await import("./credential-store.ts"); const bapiModule = await import("./bapi.ts"); -const { resolveKeylessTarget, resolveInstanceTarget, hasKeyPairMismatch } = +const { resolveKeylessTarget, resolveInstanceTarget, hasKeyPairMismatch, findLocalPublishableKey } = await import("./keyless-target.ts"); /** Encodes `.clerk.accounts.dev$` the way a real publishable key would. */ @@ -101,6 +101,51 @@ describe("keyless-target", () => { // The warning that the keyless view covers less belongs to the config // surface, so it hangs off `resolveInstanceTarget` — the resolver itself // stays silent for `whoami`, `open`, `env pull` and `doctor`. + describe("findLocalPublishableKey — name priority in env files", () => { + beforeEach(async () => { + delete process.env.NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY; + delete process.env.CLERK_PUBLISHABLE_KEY; + await writeFile( + join(tempDir, "package.json"), + JSON.stringify({ dependencies: { next: "14.0.0" } }), + ); + }); + + test("the framework-specific name wins even when the generic fallback appears later in the file", async () => { + await writeFile( + join(tempDir, ".env.local"), + "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_framework\nCLERK_PUBLISHABLE_KEY=pk_test_generic\n", + ); + + expect(await findLocalPublishableKey(tempDir)).toBe("pk_test_framework"); + }); + + test("the framework-specific name wins regardless of line order", async () => { + await writeFile( + join(tempDir, ".env.local"), + "CLERK_PUBLISHABLE_KEY=pk_test_generic\nNEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_framework\n", + ); + + expect(await findLocalPublishableKey(tempDir)).toBe("pk_test_framework"); + }); + + test("within one name a later file still overrides an earlier one", async () => { + await writeFile(join(tempDir, ".env"), "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_env\n"); + await writeFile( + join(tempDir, ".env.local"), + "NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=pk_test_env_local\n", + ); + + expect(await findLocalPublishableKey(tempDir)).toBe("pk_test_env_local"); + }); + + test("the generic fallback is still found when the framework name is absent", async () => { + await writeFile(join(tempDir, ".env.local"), "CLERK_PUBLISHABLE_KEY=pk_test_generic\n"); + + expect(await findLocalPublishableKey(tempDir)).toBe("pk_test_generic"); + }); + }); + describe("resolveInstanceTarget — not-linked warning", () => { const session = (expiresAt: number) => ({ accessToken: "at", diff --git a/packages/cli-core/src/lib/keyless-target.ts b/packages/cli-core/src/lib/keyless-target.ts index fa5fd3a6b..93316eb30 100644 --- a/packages/cli-core/src/lib/keyless-target.ts +++ b/packages/cli-core/src/lib/keyless-target.ts @@ -82,18 +82,27 @@ async function findKeyInProject(cwd: string, names: string[]): Promise(); for (const envFile of ENV_FILES) { const file = Bun.file(join(cwd, envFile)); if (!(await file.exists())) continue; for (const line of parseEnvFile(await file.text())) { if (line.type !== "entry" || !line.value) continue; - if (names.includes(line.key)) found = { value: line.value, source: envFile }; + if (names.includes(line.key)) { + foundByName.set(line.key, { value: line.value, source: envFile }); + } } } - return found; + for (const name of names) { + const located = foundByName.get(name); + if (located) return located; + } + return undefined; } /** diff --git a/packages/cli-core/src/lib/keyless.test.ts b/packages/cli-core/src/lib/keyless.test.ts index 7def4fe36..4c781b13a 100644 --- a/packages/cli-core/src/lib/keyless.test.ts +++ b/packages/cli-core/src/lib/keyless.test.ts @@ -8,6 +8,7 @@ const { parseClaimToken, writeKeylessBreadcrumb, readKeylessBreadcrumb, + peekKeylessBreadcrumb, clearKeylessBreadcrumb, writeKeysToEnvFile, createAccountlessApp, @@ -74,6 +75,20 @@ describe("breadcrumb", () => { expect(await Bun.file(breadcrumbFile).exists()).toBe(false); }); + test("peek returns the breadcrumb like read does", async () => { + await writeKeylessBreadcrumb(tempDir, "token_abc"); + const result = await peekKeylessBreadcrumb(tempDir); + expect(result?.claimToken).toBe("token_abc"); + }); + + test("peek leaves a wrong-shape file in place instead of clearing it", async () => { + const breadcrumbFile = join(tempDir, ".clerk", "keyless.json"); + await Bun.write(breadcrumbFile, JSON.stringify({ claimToken: 12345, createdAt: "2024-01-01" })); + const result = await peekKeylessBreadcrumb(tempDir); + expect(result).toBeUndefined(); + expect(await Bun.file(breadcrumbFile).exists()).toBe(true); + }); + test("clear removes the breadcrumb file", async () => { await writeKeylessBreadcrumb(tempDir, "token_abc"); await clearKeylessBreadcrumb(tempDir); diff --git a/packages/cli-core/src/lib/keyless.ts b/packages/cli-core/src/lib/keyless.ts index d87ea0b37..46db0a1a7 100644 --- a/packages/cli-core/src/lib/keyless.ts +++ b/packages/cli-core/src/lib/keyless.ts @@ -150,6 +150,21 @@ export async function readKeylessBreadcrumb(cwd: string): Promise { + try { + const data: unknown = await Bun.file(breadcrumbPath(cwd)).json(); + return isKeylessBreadcrumb(data) ? data : undefined; + } catch { + return undefined; + } +} + export async function clearKeylessBreadcrumb(cwd: string): Promise { try { await unlink(breadcrumbPath(cwd)); From b6e4477000fe56517d390b838caefac5d9c23ef6 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Thu, 30 Jul 2026 19:06:37 -0300 Subject: [PATCH 06/10] fix(keyless): address review feedback on key precedence, init guard, and doctor diagnostics - Restore CLERK_SECRET_KEY's precedence: the env var wins over a linked profile again, and --instance next to it stays a no-op instead of a usage error. Routing it through resolveKeylessTarget had silently changed both, breaking invocations that work on main; real env-var-path tests pin it now. - init's keep-the-existing-app guard also recognizes an application the SDK minted for itself (.clerk/.tmp/keyless.json), instead of silently orphaning it with a replacement. - doctor reports a malformed local secret key as one named failing check with a remedy, instead of five anonymous 'Check crashed' lines. - whoami: keyless next-steps no longer point at clerk link (which cannot work pre-claim), and an unlinked directory holding a local secret key is called out, since clerk api/users will use that key's instance, not the account. - Smaller review items: billing asserts on an already-resolved target, config schema resolves its target once, apply-patch dry-run messages reuse the verb, claim-url validation inlined, keylessTargetStubs covers every export for module mocks, positive test for the non-429 refresh refusal, keyless disable-orgs test, README corrections, changeset trimmed. --- .changeset/keyless-mode-default.md | 38 ++----------- .../cli-core/src/commands/billing/index.ts | 13 +++-- .../cli-core/src/commands/config/README.md | 2 +- .../src/commands/config/apply-patch.ts | 8 +-- .../src/commands/config/keyless.test.ts | 27 +++++++++ .../cli-core/src/commands/config/schema.ts | 9 +-- .../cli-core/src/commands/doctor/README.md | 8 ++- .../cli-core/src/commands/doctor/checks.ts | 11 ++++ .../src/commands/doctor/context.test.ts | 9 ++- .../cli-core/src/commands/doctor/context.ts | 22 +++++++- .../src/commands/doctor/doctor.test.ts | 4 +- .../cli-core/src/commands/doctor/types.ts | 8 +++ packages/cli-core/src/commands/init/index.ts | 25 ++++++--- .../src/commands/init/strategy.test.ts | 56 +++++++++++++++++++ .../cli-core/src/commands/open/index.test.ts | 3 +- .../src/commands/open/keyless-claim.ts | 28 +++++----- .../cli-core/src/commands/users/open.test.ts | 3 +- packages/cli-core/src/commands/users/open.ts | 16 +++--- .../src/commands/whoami/index.test.ts | 4 ++ .../cli-core/src/commands/whoami/index.ts | 38 ++++++++++++- .../cli-core/src/lib/bapi-command.test.ts | 38 ++++++++++++- packages/cli-core/src/lib/bapi-command.ts | 16 +++++- .../cli-core/src/lib/credential-store.test.ts | 18 ++++++ packages/cli-core/src/lib/keyless-target.ts | 9 ++- packages/cli-core/src/lib/next-steps.ts | 6 ++ .../cli-core/src/test/lib/init-harness.ts | 1 + packages/cli-core/src/test/lib/stubs.ts | 14 +++++ 27 files changed, 334 insertions(+), 100 deletions(-) diff --git a/.changeset/keyless-mode-default.md b/.changeset/keyless-mode-default.md index 3c3f0b423..aa1f30e37 100644 --- a/.changeset/keyless-mode-default.md +++ b/.changeset/keyless-mode-default.md @@ -2,37 +2,9 @@ "clerk": minor --- -Make keyless mode the default for unauthenticated `clerk init` runs on keyless-capable frameworks, and add a `--login` flag to force the authenticated flow. +Make keyless mode the default for unauthenticated `clerk init` runs on keyless-capable frameworks, and let an unclaimed keyless application be configured end to end without an account. -- Bootstrapping a new project while signed out now uses auto-generated temporary development keys instead of prompting for a browser login; running `clerk auth login` later claims the app automatically. Re-running init in an existing project while signed out still triggers the login flow. -- Agent-mode runs while signed out now set up keyless keys with a claim breadcrumb instead of printing manual setup guidance. -- `--keyless` now forces keyless mode even when signed in, including in existing projects. Combining it with `--login` or `--app` exits with a usage error. -- `--login` in agent mode while signed out exits with a usage error, since the browser login needs an interactive terminal. -- Re-running init on a project that already holds an unclaimed keyless application now keeps that application instead of silently minting a second one and orphaning the first. `--fresh` asks for a replacement explicitly; neither `-y` nor agent mode is treated as consent to discard one. -- Agent mode no longer treats the mere presence of stored credentials as being signed in. A session that turns out expired used to send an agent into a browser login it could never complete; init now validates the session and falls back to keyless. -- `--template` and `--fresh` exit with a usage error naming the reason when the run doesn't resolve to keyless, instead of being silently dropped. - -Add keyless support to `clerk config pull` and `clerk config patch`, so an unclaimed keyless application can be configured without logging in. - -- When the project isn't linked and no `--app` is passed, both commands use the instance secret key the project already holds and talk to the Backend API instead of the Platform API. This needs no account at all — no login and no `CLERK_PLATFORM_API_KEY`. If credentials happen to be present, the command still works and warns that linking would give the full configuration; when the stored session has expired, that warning says so rather than pointing at a `clerk link` that can't succeed yet. -- `clerk config patch` no longer reports a field as pushed when the Backend API dropped it. The API ignores field names it doesn't recognise inside a group rather than rejecting them, so a typo used to end in "Config pushed successfully" with nothing applied; the write is now checked against the API's own response and unrecognised fields are named. Groups whose write answers with no body are reported as unconfirmed rather than guessed at. -- Keyless payloads name Backend API resources directly: `instance`, `communication`, `restrictions`, `organization_settings`, `protect`, `oauth_application_settings`, and `instance_settings` (`test_mode`, `progressive_sign_up`, `from_email_address`, `restricted_to_allowlist`). Any other top-level key exits with a usage error naming the supported ones. -- `clerk config pull` returns the same envelope; `restrictions` and `instance_settings` are omitted because the Backend API has no read route for them. -- `clerk config patch` now rejects unknown fields on the `instance` group before sending anything. That route answers `204` with no body and silently discards field names it doesn't recognise, so `{"instance": {"password": "on"}}` used to print "Config pushed successfully" and change nothing at all. The error names the fields the route does accept, and for the auth settings people reach for here — password, phone number, username, passkeys, second factors, social providers — says that Clerk's Backend API has no route for them at all, so no retyping will help. -- `clerk config patch` no longer prints a stale re-read as the result of a write. The `instance` group answers with no body, and the follow-up read that used to fill the gap is eventually consistent — it routinely returned the pre-write value, which then appeared directly beneath "Config pushed successfully" as though the write had been ignored. The group now contributes no state, and the success line names it as unconfirmed. -- `allowlist_identifiers` and `blocklist_identifiers` in a config payload now point at `clerk api`, which reaches them on an unclaimed application today, instead of telling the user to `clerk auth login` and claim the app first. -- In keyless mode `--dry-run` previews the diff locally without sending anything, `--instance` exits with a usage error (the secret key already targets one instance), and `clerk config put` and `clerk config schema` explain that they need a claimed application. - -Let more commands operate on an unclaimed keyless application, so an agent can bootstrap and configure Clerk end to end without an account. - -- `clerk enable orgs` and `clerk disable orgs` now work without logging in, writing through the instance's own organization settings. `clerk enable/disable billing` still requires a claimed application and now says so directly, because Clerk's Backend API exposes no billing settings. -- `clerk whoami` reports the keyless instance — id, environment, publishable key, and where the key was found — instead of failing with "not logged in". -- `clerk env pull` writes a keyless application's locally-held keys into the project's env file instead of failing with "not linked". -- Every keyless-capable command now also finds the keys a Clerk SDK created for itself in `.clerk/.tmp/keyless.json`, so an application minted by running the dev server is reachable from the CLI. -- `clerk init --template ` pre-configures the keyless application at creation — a `b2b-saas` app comes back with organizations already enabled. Keyless-only; combining it with `--login` exits with a usage error. -- `clerk users`, `clerk api`, and the rest of the Backend API command surface now resolve a keyless project's own secret key from its env files. They appeared to work in development only because Bun loads `.env.local` automatically; the released binary does not, so on a real machine every one of them failed with "no secret key found" in a directory where `clerk whoami` happily named the instance. -- `clerk doctor` understands keyless projects. An unclaimed keyless application is reported as a healthy state naming the instance, instead of two failures pushing `clerk auth login` and `clerk link`; checks that genuinely need an account are skipped rather than failed. The report also says how this particular application can be claimed, since `clerk auth login` only claims silently when `clerk init` left a claim token behind. -- `clerk open` finds a keyless application's claim link instead of dead-ending on "Run `clerk link` first", which cannot claim an unclaimed application. -- `clerk whoami` and `clerk env pull` now check that a project's publishable and secret keys belong to the same application. They are found independently, so a leftover key from another project used to be reported — and written — as a matched pair, leaving the server trusting one application while the browser talked to another. `whoami` warns and still reports; `env pull` refuses to write. -- Commands no longer leak `Token refresh failed (401)` from the credential store when a stored session can't be refreshed. Any refusal from the token endpoint now reads as an expired session, and `clerk whoami` falls through to the keyless application rather than aborting. -- `clerk users open ` explains that an unclaimed application has no dashboard page — a dashboard link needs an application ID, and one is only assigned on claiming — instead of reporting that nothing is linked and pointing at `clerk link` and `--app`, neither of which can name an application that does not exist yet. The error suggests claiming the app, and `clerk api /users/` to read the user without claiming. +- `clerk init` while signed out now bootstraps with auto-generated development keys instead of forcing a browser login; running `clerk auth login` later claims the application. `--keyless` forces keyless mode even when signed in, `--login` forces the authenticated flow, and `--template ` shapes the application at creation. Re-running init keeps an existing unclaimed application — whether `clerk init` or a Clerk SDK minted it — unless `--fresh` explicitly asks for a replacement. +- `clerk config pull` and `clerk config patch` work on an unclaimed application through the Backend API, addressing its resources directly: `instance`, `communication`, `restrictions`, `organization_settings`, `protect`, `oauth_application_settings`, and `instance_settings`. Unsupported groups and unrecognized `instance` fields exit with a usage error naming what the API accepts, instead of printing success while the write was silently dropped; applied writes are verified against the API's own response. +- `clerk enable orgs`/`disable orgs`, `clerk whoami`, `clerk env pull`, `clerk doctor`, `clerk open`, `clerk users`, and `clerk api` all operate on an unclaimed keyless application, resolving the project's own secret key from its env files or the SDK's `.clerk/.tmp/keyless.json`. An exported `CLERK_SECRET_KEY` keeps its existing precedence, including over a linked profile. Commands that genuinely need a claimed application (billing, `config put`, `config schema`, `users open`) say so and name the reason instead of failing with "not linked". +- `clerk whoami` and `clerk env pull` detect a publishable/secret key pair belonging to two different applications — `whoami` warns, `env pull` refuses to write the pair. `whoami` in an unlinked directory also points out a local secret key when one is present, since `clerk api` and `clerk users` will use that key's instance rather than the signed-in account. diff --git a/packages/cli-core/src/commands/billing/index.ts b/packages/cli-core/src/commands/billing/index.ts index 93bd19d28..a9ec75eb1 100644 --- a/packages/cli-core/src/commands/billing/index.ts +++ b/packages/cli-core/src/commands/billing/index.ts @@ -59,10 +59,10 @@ function describeTargets(targets: Target[]): string { /** * Billing settings live only in the account-level config document — Clerk's * Backend API exposes no billing resource — so these commands can't run against - * an unclaimed keyless application the way the org toggles can. + * an unclaimed keyless application the way the org toggles can. Pure assertion: + * resolving the target is `resolveInstanceTarget`'s job, not billing's. */ -async function resolveBillingTarget(options: BillingOptions): Promise { - const target = await resolveInstanceTarget(options); +function assertBillingTarget(target: InstanceTarget): void { if (target.kind === "keyless") { throw new CliError( "Billing can only be configured on a claimed application — Clerk's Backend API has no billing settings, so an unclaimed keyless application can't reach them.\n" + @@ -70,12 +70,12 @@ async function resolveBillingTarget(options: BillingOptions): Promise { const targets = parseForTargets(options.for); - const target = await resolveBillingTarget(options); + const target = await resolveInstanceTarget(options); + assertBillingTarget(target); const billing: Record = {}; const payload: Record = { billing }; @@ -139,7 +139,8 @@ async function offerBillingSkillInstall(options: BillingOptions): Promise export async function billingDisable(options: BillingOptions): Promise { const targets = parseForTargets(options.for); - const target = await resolveBillingTarget(options); + const target = await resolveInstanceTarget(options); + assertBillingTarget(target); // No cascade: leave organization_settings untouched. const billing: Record = {}; diff --git a/packages/cli-core/src/commands/config/README.md b/packages/cli-core/src/commands/config/README.md index fe28631b7..3a40d3d46 100644 --- a/packages/cli-core/src/commands/config/README.md +++ b/packages/cli-core/src/commands/config/README.md @@ -215,7 +215,7 @@ Any other top-level key exits with a usage error naming the supported ones. Most `instance_settings` is backed by a beta route, and is the only way to reach those four auth-config fields without an account — which is why it's included. -`clerk config pull` returns the same envelope. `restrictions` is omitted because the Backend API has no read route for it; asking for it by name prints a warning rather than failing. +`clerk config pull` returns the same envelope. `restrictions` and `instance_settings` are omitted because the Backend API has no read route for either (see the Readable column above) — a `pull` run to confirm a write to them will not show the field, which does not mean the write failed. Asking for either by name prints a warning rather than failing. `GET /v1/instance` returns a subset of what `PATCH /v1/instance` accepts — `support_email`, for example, is writable but not readable. Fields the read omits have no "before" value to compare against, so they always appear as additions in the diff and never trigger "No changes detected". The write itself is unaffected: patching a field to the value it already holds is a no-op server-side. diff --git a/packages/cli-core/src/commands/config/apply-patch.ts b/packages/cli-core/src/commands/config/apply-patch.ts index b46d34139..b7e520145 100644 --- a/packages/cli-core/src/commands/config/apply-patch.ts +++ b/packages/cli-core/src/commands/config/apply-patch.ts @@ -41,9 +41,7 @@ export async function applyConfigPatch(opts: ApplyPatchOptions): Promise writeInstanceConfig(target, payload, { method: "PATCH", dryRun, failureContext }), ); diff --git a/packages/cli-core/src/commands/config/keyless.test.ts b/packages/cli-core/src/commands/config/keyless.test.ts index e85f830c2..6ae707159 100644 --- a/packages/cli-core/src/commands/config/keyless.test.ts +++ b/packages/cli-core/src/commands/config/keyless.test.ts @@ -712,6 +712,33 @@ describe("keyless config", () => { expect(captured.err).toContain("Organizations enabled"); }); + test("disable orgs works without an account and without the billing pre-flight", async () => { + // Start from orgs enabled, or the disable is a no-op and nothing is sent. + bapiState["/v1/instance/organization_settings"] = { ...ORG_SETTINGS, enabled: true }; + const requests: string[] = []; + stubFetch(async (input, init) => { + const path = input.toString().replace(BAPI_URL, ""); + const method = (init?.method ?? "GET").toUpperCase(); + requests.push(`${method} ${path}`); + if (method === "PATCH") { + bapiState[path] = { ...(bapiState[path] as object), ...JSON.parse(init?.body as string) }; + } + return new Response(JSON.stringify(bapiState[path] ?? ORG_SETTINGS), { status: 200 }); + }); + const { orgsDisable } = await import("../orgs/index.ts"); + + // Keyless has no account config document, so `current` is undefined and + // the org-billing pre-flight is skipped — this pins that the disable + // path tolerates that instead of reaching for `current.billing`. + await orgsDisable({ yes: true }); + + expect(requests).toContain("PATCH /v1/instance/organization_settings"); + // Every request is a bare BAPI path (the BAPI base URL was stripped) — + // a Platform API config fetch would show up here as a full foreign URL. + expect(requests.every((line) => / \/v1\//.test(line))).toBe(true); + expect(captured.err).toContain("Organizations disabled"); + }); + test("whoami reports the keyless instance instead of demanding a login", async () => { const { whoami } = await import("../whoami/index.ts"); diff --git a/packages/cli-core/src/commands/config/schema.ts b/packages/cli-core/src/commands/config/schema.ts index 130551c43..09e8fbf62 100644 --- a/packages/cli-core/src/commands/config/schema.ts +++ b/packages/cli-core/src/commands/config/schema.ts @@ -1,9 +1,8 @@ -import { resolveAppContext } from "../../lib/config.ts"; import { fetchInstanceConfigSchema } from "../../lib/plapi.ts"; import { CliError, ERROR_CODE, withApiContext } from "../../lib/errors.ts"; import { withGutter } from "../../lib/spinner.ts"; import { log } from "../../lib/log.ts"; -import { resolveKeylessTarget } from "../../lib/keyless-target.ts"; +import { resolveInstanceTarget } from "../../lib/keyless-target.ts"; interface ConfigSchemaOptions { app?: string; @@ -14,7 +13,9 @@ interface ConfigSchemaOptions { export async function configSchema(options: ConfigSchemaOptions): Promise { await withGutter("Fetching configuration schema", async () => { - if (await resolveKeylessTarget(options)) { + // Same shape as config push/put: resolve the target once, branch on kind. + const target = await resolveInstanceTarget(options); + if (target.kind === "keyless") { throw new CliError( "Config schema is only available for a claimed application — the schema describes the account-level config document, which an unclaimed keyless application has no access to.\n" + "Run `clerk auth login` to claim this application, then re-run `clerk config schema`.", @@ -22,7 +23,7 @@ export async function configSchema(options: ConfigSchemaOptions): Promise ); } - const ctx = await resolveAppContext(options); + const ctx = target.ctx; log.info(`Pulling config schema from ${ctx.appLabel} (${ctx.instanceLabel})...`); diff --git a/packages/cli-core/src/commands/doctor/README.md b/packages/cli-core/src/commands/doctor/README.md index 8d744ba1d..443043cc7 100644 --- a/packages/cli-core/src/commands/doctor/README.md +++ b/packages/cli-core/src/commands/doctor/README.md @@ -46,8 +46,12 @@ on disk (or in `CLERK_SECRET_KEY`/framework env var), is running on an **unclaimed keyless application** — a legitimate, healthy state, not a broken one. -- No token, keyless key present → **pass**, naming the instance and noting - `clerk auth login` claims it. +- No token, keyless key present → **pass**, naming the instance. The claim + hint depends on where the app came from: with a `.clerk/keyless.json` + breadcrumb (left by `clerk init`) it says `clerk auth login` claims it; + without one — an SDK-minted `.clerk/.tmp/keyless.json`, or a hand-copied + `CLERK_SECRET_KEY` — it says to claim from the Clerk Dashboard instead, + because `clerk auth login` only auto-claims apps `clerk init` created. - Stored session expired, keyless key present → **warn** (not fail): the keyless key still works, logging in again is optional. - Signed in (has account credentials) but this directory isn't linked, keyless diff --git a/packages/cli-core/src/commands/doctor/checks.ts b/packages/cli-core/src/commands/doctor/checks.ts index 56be15ea7..5224051c3 100644 --- a/packages/cli-core/src/commands/doctor/checks.ts +++ b/packages/cli-core/src/commands/doctor/checks.ts @@ -107,6 +107,17 @@ export async function checkLoggedIn(ctx: DoctorContext): Promise { ); } + // A local key that isn't a secret key at all is the one keyless state that + // is genuinely broken — report it here as the named diagnosis, once, rather + // than letting it crash every keyless-aware check (see getKeylessKeyError). + const keyError = await ctx.getKeylessKeyError(); + if (keyError) { + return check.fail(`Not logged in, and the local secret key is unusable: ${keyError.message}`, { + remedy: "Fix or remove the malformed key, or run `clerk auth login` to authenticate.", + fixable: false, + }); + } + return check.fail("Not logged in", { remedy: "Run `clerk auth login` to authenticate.", }); diff --git a/packages/cli-core/src/commands/doctor/context.test.ts b/packages/cli-core/src/commands/doctor/context.test.ts index fd81fa4dc..878aa731f 100644 --- a/packages/cli-core/src/commands/doctor/context.test.ts +++ b/packages/cli-core/src/commands/doctor/context.test.ts @@ -1,5 +1,11 @@ import { test, expect, describe, mock, spyOn, beforeEach, afterEach, afterAll } from "bun:test"; -import { useCaptureLog, credentialStoreStubs, gitStubs, stubFetch } from "../../test/lib/stubs.ts"; +import { + useCaptureLog, + credentialStoreStubs, + gitStubs, + keylessTargetStubs, + stubFetch, +} from "../../test/lib/stubs.ts"; import * as config from "../../lib/config.ts"; import type { Application } from "../../lib/plapi.ts"; @@ -22,6 +28,7 @@ mock.module("../../lib/git.ts", () => gitStubs); const mockResolveKeylessTarget = mock(); mock.module("../../lib/keyless-target.ts", () => ({ + ...keylessTargetStubs, resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), })); diff --git a/packages/cli-core/src/commands/doctor/context.ts b/packages/cli-core/src/commands/doctor/context.ts index 83b579a3c..a9abb6881 100644 --- a/packages/cli-core/src/commands/doctor/context.ts +++ b/packages/cli-core/src/commands/doctor/context.ts @@ -5,7 +5,7 @@ import { resolveKeylessTarget, type KeylessTarget } from "../../lib/keyless-targ import { peekKeylessBreadcrumb } from "../../lib/keyless.ts"; import { bapiRequest } from "../../lib/bapi.ts"; import { log } from "../../lib/log.ts"; -import { errorMessage } from "../../lib/errors.ts"; +import { CliError, ERROR_CODE, errorMessage } from "../../lib/errors.ts"; import type { DoctorContext, KeylessInstanceInfo, ResolvedProfile } from "./types.ts"; export function createDoctorContext(): DoctorContext { @@ -16,6 +16,7 @@ export function createDoctorContext(): DoctorContext { let keylessPromise: Promise | undefined; let keylessInstancePromise: Promise | undefined; let claimBreadcrumbPromise: Promise | undefined; + let keylessKeyError: CliError | undefined; const ctx: DoctorContext = { getToken() { @@ -56,11 +57,28 @@ export function createDoctorContext(): DoctorContext { if (!keylessPromise) { // Same resolution every other command uses: an explicit --app/link rules // keyless out, but account credentials alone don't (see keyless-target.ts). - keylessPromise = resolveKeylessTarget({ cwd: process.cwd() }); + // + // A malformed local key (not `sk_`-prefixed) is caught here rather than + // propagated: every keyless-aware check calls this getter, so letting it + // throw turns one misconfiguration into a "Check crashed" line per check, + // each stripped of its check name. It's cached as a diagnosable state + // instead, and checkLoggedIn reports it once, by name, with a remedy. + keylessPromise = resolveKeylessTarget({ cwd: process.cwd() }).catch((error) => { + if (error instanceof CliError && error.code === ERROR_CODE.INVALID_KEY_FORMAT) { + keylessKeyError = error; + return undefined; + } + throw error; + }); } return keylessPromise; }, + async getKeylessKeyError() { + await ctx.getKeylessTarget(); + return keylessKeyError; + }, + getKeylessInstance() { if (!keylessInstancePromise) { keylessInstancePromise = (async () => { diff --git a/packages/cli-core/src/commands/doctor/doctor.test.ts b/packages/cli-core/src/commands/doctor/doctor.test.ts index 0d51bd140..89425cde4 100644 --- a/packages/cli-core/src/commands/doctor/doctor.test.ts +++ b/packages/cli-core/src/commands/doctor/doctor.test.ts @@ -2,7 +2,7 @@ import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { ApiError, AuthError } from "../../lib/errors.ts"; +import { ApiError, AuthError, type CliError } from "../../lib/errors.ts"; import { _setConfigDir } from "../../lib/config.ts"; import { credentialStoreStubs, @@ -108,6 +108,7 @@ function createMockContext( keylessTarget?: KeylessTarget; keylessInstance?: KeylessInstanceInfo | null; claimBreadcrumb?: boolean; + keylessKeyError?: CliError; } = {}, ): DoctorContext { return { @@ -124,6 +125,7 @@ function createMockContext( getKeylessTarget: async () => overrides.keylessTarget, getKeylessInstance: async () => overrides.keylessInstance ?? null, hasClaimBreadcrumb: async () => overrides.claimBreadcrumb ?? false, + getKeylessKeyError: async () => overrides.keylessKeyError, fixes: { login: noopFix, link: noopFix, diff --git a/packages/cli-core/src/commands/doctor/types.ts b/packages/cli-core/src/commands/doctor/types.ts index 4b13db359..1b665b8e8 100644 --- a/packages/cli-core/src/commands/doctor/types.ts +++ b/packages/cli-core/src/commands/doctor/types.ts @@ -1,4 +1,5 @@ import type { resolveProfile } from "../../lib/config.ts"; +import type { CliError } from "../../lib/errors.ts"; import type { Application } from "../../lib/plapi.ts"; import type { KeylessTarget } from "../../lib/keyless-target.ts"; @@ -39,6 +40,13 @@ export interface DoctorContext { getKeylessTarget(): Promise; /** Best-effort identity of the keyless instance, for naming it in check output. */ getKeylessInstance(): Promise; + /** + * The malformed-local-key error `getKeylessTarget()` swallowed, if any. A + * key that doesn't start with `sk_` is precisely the misconfiguration doctor + * exists to diagnose, so it surfaces as one named failing check instead of + * crashing every keyless-aware check anonymously. + */ + getKeylessKeyError(): Promise; /** * Whether a `clerk init` claim breadcrumb is present, read once and without * side effects — `readKeylessBreadcrumb` clears a malformed file as it goes, diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index 44c7d79be..424abdbc8 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -25,6 +25,7 @@ import { KEYLESS_TEMPLATES, type KeylessTemplate, } from "../../lib/keyless.js"; +import { readSdkKeylessApp } from "../../lib/keyless-target.ts"; import { printNextSteps } from "../../lib/next-steps.js"; import { gatherContext, hasPackageJson } from "./context.js"; import { scaffold, enrichProjectContext } from "./scaffold.js"; @@ -465,13 +466,16 @@ async function authenticateAndLink( /** * A `.clerk/keyless.json` breadcrumb means an earlier run already minted an - * unclaimed keyless application for this project — its claim token, and - * anything configured on it or created in it, only exist as long as that - * breadcrumb (and the env keys pointing at it) survive. Re-running init must - * not silently mint a replacement and orphan that app, so this asks before - * ever touching it: human mode confirms (default: keep); agent mode and `-y` - * both keep it too, since neither can consent to a destructive default. - * `--fresh` is the explicit "I know, replace it anyway" escape hatch. + * unclaimed keyless application for this project — its claim token, and the + * local means of claiming or reaching it, only exist as long as that + * breadcrumb (and the env keys pointing at it) survive. The same is true of + * an application a Clerk SDK minted for itself in `.clerk/.tmp/keyless.json` + * (running `next dev` with no keys configured), so both files count as "an + * app already exists here". Re-running init must not silently mint a + * replacement and orphan either one, so this asks before ever touching it: + * human mode confirms (default: keep); agent mode and `-y` both keep it too, + * since neither can consent to a destructive default. `--fresh` is the + * explicit "I know, replace it anyway" escape hatch. */ async function shouldKeepExistingKeyless( cwd: string, @@ -481,12 +485,15 @@ async function shouldKeepExistingKeyless( if (fresh) return false; const existing = await readKeylessBreadcrumb(cwd); - if (!existing) return false; + const sdkApp = existing ? undefined : await readSdkKeylessApp(cwd); + if (!existing && !sdkApp?.secretKey) return false; if (skipConfirm) return true; const replace = await confirm({ - message: `This project already has an unclaimed keyless application (created ${existing.createdAt}). Replace it with a new one?`, + message: existing + ? `This project already has an unclaimed keyless application (created ${existing.createdAt}). Replace it with a new one?` + : "This project already has an unclaimed keyless application (minted by its Clerk SDK in `.clerk/.tmp/keyless.json`). Replace it with a new one?", default: false, }); return !replace; diff --git a/packages/cli-core/src/commands/init/strategy.test.ts b/packages/cli-core/src/commands/init/strategy.test.ts index 10976d982..472c9f9ba 100644 --- a/packages/cli-core/src/commands/init/strategy.test.ts +++ b/packages/cli-core/src/commands/init/strategy.test.ts @@ -20,6 +20,7 @@ import { heuristics, bootstrapMod, keylessMod, + keylessTargetMod, } from "../../test/lib/init-harness.ts"; import * as promptsMod from "../../lib/prompts.ts"; import { init } from "./index.ts"; @@ -528,6 +529,61 @@ describe("init strategy", () => { expect(printExistingSpy).toHaveBeenCalledWith(KEYLESS_CTX.envFile); }); + test("an app the SDK minted for itself counts as existing too — no silent replacement", async () => { + setup({ isAgent: true, email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + // No CLI breadcrumb — the app came from running the dev server, so the + // only trace is the SDK's own .clerk/.tmp/keyless.json. + const breadcrumbSpy = spyOn(keylessMod, "readKeylessBreadcrumb").mockResolvedValue(undefined); + const sdkAppSpy = spyOn(keylessTargetMod, "readSdkKeylessApp").mockResolvedValue({ + secretKey: "sk_test_sdkapp", + publishableKey: "pk_test_sdkapp", + }); + const confirmSpy = spyOn(promptsMod, "confirm"); + const printExistingSpy = spyOn(heuristics, "printExistingKeylessInfo").mockReturnValue( + undefined, + ); + track(breadcrumbSpy); + track(sdkAppSpy); + track(confirmSpy); + track(printExistingSpy); + + await init({}); + + expect(confirmSpy).not.toHaveBeenCalled(); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(printExistingSpy).toHaveBeenCalledWith(KEYLESS_CTX.envFile); + }); + + test("human mode names the SDK file when prompting to replace an SDK-minted app", async () => { + setup({ email: null }); + mockExistingProject(KEYLESS_CTX); + mockMiddlewareScaffold(); + const breadcrumbSpy = spyOn(keylessMod, "readKeylessBreadcrumb").mockResolvedValue(undefined); + const sdkAppSpy = spyOn(keylessTargetMod, "readSdkKeylessApp").mockResolvedValue({ + secretKey: "sk_test_sdkapp", + }); + const confirmSpy = spyOn(promptsMod, "confirm").mockResolvedValue(false); + const printExistingSpy = spyOn(heuristics, "printExistingKeylessInfo").mockReturnValue( + undefined, + ); + track(breadcrumbSpy); + track(sdkAppSpy); + track(confirmSpy); + track(printExistingSpy); + + await init({ keyless: true }); + + expect(confirmSpy).toHaveBeenCalledWith( + expect.objectContaining({ + message: expect.stringContaining(".clerk/.tmp/keyless.json"), + default: false, + }), + ); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + }); + test("human mode mints a fresh app when the user confirms replacement", async () => { setup({ email: null }); mockExistingProject(KEYLESS_CTX); diff --git a/packages/cli-core/src/commands/open/index.test.ts b/packages/cli-core/src/commands/open/index.test.ts index 56e0ef9c9..be7a71711 100644 --- a/packages/cli-core/src/commands/open/index.test.ts +++ b/packages/cli-core/src/commands/open/index.test.ts @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach, beforeEach, mock } from "bun:test"; import { setMode } from "../../mode.ts"; import { setCurrentEnv } from "../../lib/environment.ts"; -import { configStubs, useCaptureLog } from "../../test/lib/stubs.ts"; +import { configStubs, keylessTargetStubs, useCaptureLog } from "../../test/lib/stubs.ts"; import { isKnownDashboardPath } from "./dashboard-paths.ts"; const mockResolveProfile = mock(); @@ -26,6 +26,7 @@ mock.module("../../lib/spinner.ts", () => ({ })); mock.module("../../lib/keyless-target.ts", () => ({ + ...keylessTargetStubs, resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), })); diff --git a/packages/cli-core/src/commands/open/keyless-claim.ts b/packages/cli-core/src/commands/open/keyless-claim.ts index e363ab095..a9b6a19d2 100644 --- a/packages/cli-core/src/commands/open/keyless-claim.ts +++ b/packages/cli-core/src/commands/open/keyless-claim.ts @@ -41,7 +41,19 @@ async function readSdkClaimUrl(cwd: string): Promise { try { const parsed = (await file.json()) as { claimUrl?: unknown }; const claimUrl = typeof parsed.claimUrl === "string" ? parsed.claimUrl : undefined; - if (claimUrl && !isHttpsUrl(claimUrl)) { + if (!claimUrl) return undefined; + + // Written by another process, so untrusted input: anything but well-formed + // https is dropped rather than handed to the browser launcher — which on + // Windows goes through `cmd /c start "" ""`, where a `file:`/ + // `javascript:` scheme or an embedded quote stops being just a URL. + let isHttps = false; + try { + isHttps = new URL(claimUrl).protocol === "https:"; + } catch { + // fall through: not a URL at all + } + if (!isHttps) { log.debug(`open: ignoring non-https claimUrl in ${SDK_KEYLESS_FILE.join("/")}`); return undefined; } @@ -52,20 +64,6 @@ async function readSdkClaimUrl(cwd: string): Promise { } } -/** - * The SDK's file is written by another process, so its URL is untrusted input. - * Anything but well-formed https is dropped rather than handed to the browser - * launcher — which on Windows goes through `cmd /c start "" ""`, where a - * `file:`/`javascript:` scheme or an embedded quote stops being just a URL. - */ -function isHttpsUrl(value: string): boolean { - try { - return new URL(value).protocol === "https:"; - } catch { - return false; - } -} - /** * Finds the claim link for an unclaimed keyless application, checking both * places one can turn up depending on how keyless mode was entered: diff --git a/packages/cli-core/src/commands/users/open.test.ts b/packages/cli-core/src/commands/users/open.test.ts index d520a279e..f1af97082 100644 --- a/packages/cli-core/src/commands/users/open.test.ts +++ b/packages/cli-core/src/commands/users/open.test.ts @@ -1,7 +1,7 @@ import { test, expect, describe, afterEach, beforeEach, mock } from "bun:test"; import { setMode } from "../../mode.ts"; import { setCurrentEnv } from "../../lib/environment.ts"; -import { configStubs, useCaptureLog } from "../../test/lib/stubs.ts"; +import { configStubs, keylessTargetStubs, useCaptureLog } from "../../test/lib/stubs.ts"; const mockResolveAppContext = mock(); const mockResolveProfile = mock(); @@ -32,6 +32,7 @@ mock.module("../../lib/open.ts", () => ({ const mockResolveKeylessTarget = mock(); mock.module("../../lib/keyless-target.ts", () => ({ + ...keylessTargetStubs, resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), })); diff --git a/packages/cli-core/src/commands/users/open.ts b/packages/cli-core/src/commands/users/open.ts index 98309abfc..7b959b5d3 100644 --- a/packages/cli-core/src/commands/users/open.ts +++ b/packages/cli-core/src/commands/users/open.ts @@ -80,14 +80,14 @@ async function assertNotUnclaimedKeyless(options: UsersOpenOptions, userId: stri if (options.app || options.instance) return; const keyless = await resolveKeylessTarget({ cwd: process.cwd() }); - if (!keyless) return; - - throw new CliError( - `This directory holds an unclaimed keyless application (secret key from ${keyless.source}), which has no Dashboard page — a dashboard link needs an application ID, and one is only assigned when the application is claimed.\n` + - `Run \`clerk auth login\` to claim it, then \`clerk users open ${userId}\` will work.\n` + - `To inspect the user right now, \`clerk api /users/${userId}\` reads it straight from the instance.`, - { code: ERROR_CODE.INSTANCE_NOT_FOUND }, - ); + if (keyless) { + throw new CliError( + `This directory holds an unclaimed keyless application (secret key from ${keyless.source}), which has no Dashboard page — a dashboard link needs an application ID, and one is only assigned when the application is claimed.\n` + + `Run \`clerk auth login\` to claim it, then \`clerk users open ${userId}\` will work.\n` + + `To inspect the user right now, \`clerk api /users/${userId}\` reads it straight from the instance.`, + { code: ERROR_CODE.INSTANCE_NOT_FOUND }, + ); + } } export async function open(options: UsersOpenOptions = {}): Promise { diff --git a/packages/cli-core/src/commands/whoami/index.test.ts b/packages/cli-core/src/commands/whoami/index.test.ts index 214a8a628..d5bbe9cb1 100644 --- a/packages/cli-core/src/commands/whoami/index.test.ts +++ b/packages/cli-core/src/commands/whoami/index.test.ts @@ -2,6 +2,7 @@ import { test, expect, describe, beforeEach, afterEach, mock, spyOn } from "bun: import { configStubs, credentialStoreStubs, + keylessTargetStubs, tokenExchangeStubs, useCaptureLog, } from "../../test/lib/stubs.ts"; @@ -24,6 +25,7 @@ mock.module("../../lib/credential-store.ts", () => ({ })); mock.module("../../lib/keyless-target.ts", () => ({ + ...keylessTargetStubs, resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), findLocalPublishableKey: (...args: unknown[]) => mockFindLocalPublishableKey(...args), hasKeyPairMismatch: (...args: unknown[]) => mockHasKeyPairMismatch(...args), @@ -183,6 +185,7 @@ describe("whoami", () => { const payload = JSON.parse(captured.out); expect(payload).toEqual({ email: "alice@example.com", + localSecretKeySource: null, linked: { appId: "app_xxx", appName: "MyApp", @@ -204,6 +207,7 @@ describe("whoami", () => { expect(JSON.parse(captured.out)).toEqual({ email: "alice@example.com", + localSecretKeySource: null, linked: null, }); }); diff --git a/packages/cli-core/src/commands/whoami/index.ts b/packages/cli-core/src/commands/whoami/index.ts index 4ece7d6f5..5dbe14233 100644 --- a/packages/cli-core/src/commands/whoami/index.ts +++ b/packages/cli-core/src/commands/whoami/index.ts @@ -10,6 +10,7 @@ import { isAgent } from "../../mode.ts"; import { bapiRequest } from "../../lib/bapi.ts"; import { findLocalPublishableKey, + findLocalSecretKey, hasKeyPairMismatch, resolveKeylessTarget, type KeylessTarget, @@ -25,7 +26,19 @@ export interface WhoamiOptions { * this first keeps rendering to a single place. */ type Identity = - | { kind: "account"; email: string; profile: Awaited> } + | { + kind: "account"; + email: string; + profile: Awaited>; + /** + * Where a local secret key was found when the directory is unlinked, or + * null. It has to be surfaced here because the answer to "who am I?" is + * genuinely split in that state: this command reports the account, but + * `clerk api` and `clerk users` resolve the local key first and operate + * on whatever instance it addresses (see `resolveBapiSecretKey`). + */ + localSecretKeySource: string | null; + } | { kind: "keyless"; instanceId: string | null; @@ -87,7 +100,17 @@ async function resolveIdentity(): Promise { profile = undefined; } - return { kind: "account", email: userInfo.email, profile }; + // Unlinked but holding a local secret key: the Backend API commands will use + // that key, not this account, so whoami must say so or its answer is wrong + // for half the command surface. + const localKey = profile ? undefined : await findLocalSecretKey(process.cwd()); + + return { + kind: "account", + email: userInfo.email, + profile, + localSecretKeySource: localKey?.source ?? null, + }; } async function describeKeyless(keyless: KeylessTarget): Promise { @@ -148,6 +171,7 @@ function toJson(identity: Identity): Record { const resolved = identity.profile; return { email: identity.email, + localSecretKeySource: identity.localSecretKeySource, linked: resolved ? { appId: resolved.profile.appId, @@ -169,13 +193,21 @@ function render(identity: Identity): void { log.info( `Not logged in — running on an unclaimed keyless application (key from \`${identity.keySource}\`)`, ); - printNextSteps(NEXT_STEPS.WHOAMI); + // Not NEXT_STEPS.WHOAMI: `clerk link` needs an application in an account, + // which an unclaimed app by definition isn't — the same dead end + // `users open`, `doctor`, and `open` all refuse to point at. + printNextSteps(NEXT_STEPS.WHOAMI_KEYLESS); return; } log.data(identity.email); if (identity.profile) { log.info(`Linked to \`${profileLabel(identity.profile.profile)}\``); + } else if (identity.localSecretKeySource) { + log.warn( + `This directory isn't linked, but holds a secret key (from \`${identity.localSecretKeySource}\`) — \`clerk api\` and \`clerk users\` will use that key's instance, not your account.\n` + + "Run `clerk link` to point them at an application of yours, or remove the key.", + ); } printNextSteps(identity.profile ? NEXT_STEPS.WHOAMI_LINKED : NEXT_STEPS.WHOAMI); } diff --git a/packages/cli-core/src/lib/bapi-command.test.ts b/packages/cli-core/src/lib/bapi-command.test.ts index a7fd782fb..a984b128a 100644 --- a/packages/cli-core/src/lib/bapi-command.test.ts +++ b/packages/cli-core/src/lib/bapi-command.test.ts @@ -198,13 +198,45 @@ describe("bapi-command", () => { expect(fetchApplicationSpy).toHaveBeenCalledWith("app_123"); }); + test("an exported CLERK_SECRET_KEY wins over a linked profile", async () => { + process.env.CLERK_SECRET_KEY = "sk_env_123"; + + await expect(resolveBapiSecretKey({})).resolves.toBe("sk_env_123"); + + // The env var is explicit user intent: neither the keyless resolver (which + // stands down for linked directories) nor the profile lookup runs at all. + expect(resolveKeylessTargetSpy).not.toHaveBeenCalled(); + expect(resolveAppContextSpy).not.toHaveBeenCalled(); + expect(fetchApplicationSpy).not.toHaveBeenCalled(); + }); + + test("--instance stays a no-op next to an exported CLERK_SECRET_KEY", async () => { + process.env.CLERK_SECRET_KEY = "sk_env_123"; + + // The key addresses exactly one instance, so --instance has always been + // ignored here — never a usage error. + await expect(resolveBapiSecretKey({ instance: "prod" })).resolves.toBe("sk_env_123"); + + expect(resolveKeylessTargetSpy).not.toHaveBeenCalled(); + }); + + test("rejects an exported CLERK_SECRET_KEY that is not a secret key", async () => { + process.env.CLERK_SECRET_KEY = "pk_test_not_secret"; + validateKeyPrefixSpy.mockImplementation(() => { + throw new Error("bad prefix"); + }); + + await expect(resolveBapiSecretKey({})).rejects.toThrow("bad prefix"); + expect(validateKeyPrefixSpy).toHaveBeenCalledWith("pk_test_not_secret", "sk_"); + }); + test("falls back to the shared keyless resolution when no explicit app or secret key is provided", async () => { resolveKeylessTargetSpy.mockResolvedValue({ - secretKey: "sk_env_123", - source: "CLERK_SECRET_KEY env var", + secretKey: "sk_keyless_123", + source: ".env.local", }); - await expect(resolveBapiSecretKey({ instance: "dev" })).resolves.toBe("sk_env_123"); + await expect(resolveBapiSecretKey({ instance: "dev" })).resolves.toBe("sk_keyless_123"); expect(resolveKeylessTargetSpy).toHaveBeenCalledWith({ instance: "dev", cwd: undefined }); expect(resolveAppContextSpy).not.toHaveBeenCalled(); diff --git a/packages/cli-core/src/lib/bapi-command.ts b/packages/cli-core/src/lib/bapi-command.ts index e44366484..26f6f24a4 100644 --- a/packages/cli-core/src/lib/bapi-command.ts +++ b/packages/cli-core/src/lib/bapi-command.ts @@ -79,8 +79,20 @@ export async function resolveBapiSecretKey(options: ResolveBapiSecretKeyOptions) return resolved.instance.secret_key; } - // An unclaimed keyless application keeps its only secret key on disk (env var, - // .env.local, or the SDK's own keyless.json) — the same resolution `whoami`, + // An explicitly exported CLERK_SECRET_KEY wins over everything below, + // including a linked profile — same precedence this command family has + // always had. Routing it through resolveKeylessTarget instead would lose + // both halves of that contract: the keyless resolver stands down entirely + // when the directory is linked, and refuses --instance, which has always + // been a no-op next to an env key that addresses exactly one instance. + const envSecretKey = process.env.CLERK_SECRET_KEY; + if (envSecretKey) { + validateKeyPrefix(envSecretKey, "sk_"); + return envSecretKey; + } + + // An unclaimed keyless application keeps its only secret key on disk + // (.env.local, or the SDK's own keyless.json) — the same resolution `whoami`, // `config`, and `env pull` already share. The shipped binary is compiled with // --no-compile-autoload-dotenv, so without this every `users`/`api` command // would report "no secret key" on a perfectly live keyless project the moment diff --git a/packages/cli-core/src/lib/credential-store.test.ts b/packages/cli-core/src/lib/credential-store.test.ts index b50df9270..cc58b7f79 100644 --- a/packages/cli-core/src/lib/credential-store.test.ts +++ b/packages/cli-core/src/lib/credential-store.test.ts @@ -169,6 +169,24 @@ describe("credential-store", () => { expect(await getStoredSession()).toEqual(session); }); + test("getValidToken reads a non-429 4xx refusal as an expired session but keeps the credentials", async () => { + const session = { + accessToken: "expired-access-token", + refreshToken: "refresh-token", + expiresAt: Date.now() - 60_000, + tokenType: "Bearer", + }; + await storeToken(session); + + mockRefreshAccessToken.mockRejectedValue(new ApiError(401, '{"error":"invalid_client"}')); + + // Unlike the invalid_grant branch above, the credentials stay in place: + // the refusal names this client/request, not the refresh token itself, so + // deleting the session would destroy state a config fix could still save. + await expect(getValidToken()).rejects.toBeInstanceOf(AuthError); + expect(await getStoredSession()).toEqual(session); + }); + test("createOAuthSession requires a refresh token in the auth response", () => { expect(() => createOAuthSession({ diff --git a/packages/cli-core/src/lib/keyless-target.ts b/packages/cli-core/src/lib/keyless-target.ts index 93316eb30..c1f7f5b08 100644 --- a/packages/cli-core/src/lib/keyless-target.ts +++ b/packages/cli-core/src/lib/keyless-target.ts @@ -47,8 +47,13 @@ const ENV_FILES = [".env", ".env.local"]; */ const SDK_KEYLESS_FILE = [".clerk", ".tmp", "keyless.json"]; -/** Reads the SDK's own keyless file, ignoring a partially-written one. */ -async function readSdkKeylessApp( +/** + * Reads the SDK's own keyless file, ignoring a partially-written one. + * Exported for `init`, whose keep-the-existing-app guard must recognise an + * application the SDK minted for itself just as readily as one `clerk init` + * minted — the two files describe the same kind of unclaimed app. + */ +export async function readSdkKeylessApp( cwd: string, ): Promise<{ secretKey?: string; publishableKey?: string } | undefined> { const file = Bun.file(join(cwd, ...SDK_KEYLESS_FILE)); diff --git a/packages/cli-core/src/lib/next-steps.ts b/packages/cli-core/src/lib/next-steps.ts index 807e507ba..e447ddd2e 100644 --- a/packages/cli-core/src/lib/next-steps.ts +++ b/packages/cli-core/src/lib/next-steps.ts @@ -61,6 +61,12 @@ export const NEXT_STEPS = { CONFIG_DRY_RUN_PUT: ["Run `clerk config put` without `--dry-run` to apply these changes"], LOGOUT: ["Run `clerk auth login` to sign in again"], WHOAMI: ["Run `clerk link` to connect this directory to an application"], + // An unclaimed keyless app can't be `clerk link`ed — nothing in any account + // to link to. `clerk open` finds the claim link whichever file minted the app. + WHOAMI_KEYLESS: [ + "Run `clerk open` to open this application's claim link", + "Run `clerk auth login` to connect your Clerk account", + ], WHOAMI_LINKED: [ "Run `clerk apps list` to see your other applications", "Run `clerk config pull` to inspect the live configuration of this instance", diff --git a/packages/cli-core/src/test/lib/init-harness.ts b/packages/cli-core/src/test/lib/init-harness.ts index f47e03c6c..5db6e2d9f 100644 --- a/packages/cli-core/src/test/lib/init-harness.ts +++ b/packages/cli-core/src/test/lib/init-harness.ts @@ -28,6 +28,7 @@ export * as skillsMod from "../../commands/init/skills.ts"; export * as bootstrapMod from "../../commands/init/bootstrap.ts"; export * as nextStepsMod from "../../lib/next-steps.ts"; export * as keylessMod from "../../lib/keyless.ts"; +export * as keylessTargetMod from "../../lib/keyless-target.ts"; import * as loginModule from "../../commands/auth/login.ts"; import * as linkModule from "../../commands/link/index.ts"; diff --git a/packages/cli-core/src/test/lib/stubs.ts b/packages/cli-core/src/test/lib/stubs.ts index 13e6f8db1..70598ff99 100644 --- a/packages/cli-core/src/test/lib/stubs.ts +++ b/packages/cli-core/src/test/lib/stubs.ts @@ -160,6 +160,20 @@ export const configStubs = { profile.appName ? `${profile.appName} (${profile.appId})` : profile.appId, }; +// Same wholesale-replacement rule as configStubs: this must cover every +// export of lib/keyless-target.ts, or importing it anywhere in the process +// after the mock registers becomes an import error. Spread it into each +// `mock.module("../../lib/keyless-target.ts", ...)` and override the exports +// the file under test actually exercises. +export const keylessTargetStubs = { + resolveKeylessTarget: noop, + resolveInstanceTarget: noop, + findLocalSecretKey: noop, + findLocalPublishableKey: noop, + hasKeyPairMismatch: async () => false, + readSdkKeylessApp: noop, +}; + export const autolinkStubs = { findClerkKeys: async () => [], matchKeyToApp: () => undefined, From 389e5f61c99ae9133a83601a5f2c0d3395a140e1 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Thu, 30 Jul 2026 19:18:46 -0300 Subject: [PATCH 07/10] refactor(keyless): gather user-facing keyless copy into lib/copy.ts One function per sentence with typed parameters, so a future i18n layer can swap the module's bodies without touching call sites; pluralisation and agreement rules move with the prose. No visible string changes. --- .../cli-core/src/commands/billing/index.ts | 9 ++- .../cli-core/src/commands/config/keyless.ts | 38 +++++------ packages/cli-core/src/commands/config/push.ts | 9 ++- .../cli-core/src/commands/config/schema.ts | 9 ++- packages/cli-core/src/commands/users/open.ts | 10 ++- packages/cli-core/src/lib/copy.ts | 64 +++++++++++++++++++ 6 files changed, 95 insertions(+), 44 deletions(-) create mode 100644 packages/cli-core/src/lib/copy.ts diff --git a/packages/cli-core/src/commands/billing/index.ts b/packages/cli-core/src/commands/billing/index.ts index a9ec75eb1..aec98bfb1 100644 --- a/packages/cli-core/src/commands/billing/index.ts +++ b/packages/cli-core/src/commands/billing/index.ts @@ -1,6 +1,7 @@ import { CliError, ERROR_CODE, throwUsageError } from "../../lib/errors.ts"; import { isAgent, isHuman } from "../../mode.ts"; import { log } from "../../lib/log.ts"; +import { keylessCopy } from "../../lib/copy.ts"; import { confirm } from "../../lib/prompts.ts"; import { detectPackageManager } from "../../lib/package-manager.ts"; import { NEXT_STEPS } from "../../lib/next-steps.ts"; @@ -64,11 +65,9 @@ function describeTargets(targets: Target[]): string { */ function assertBillingTarget(target: InstanceTarget): void { if (target.kind === "keyless") { - throw new CliError( - "Billing can only be configured on a claimed application — Clerk's Backend API has no billing settings, so an unclaimed keyless application can't reach them.\n" + - "Run `clerk auth login` to claim this application, then re-run the command.", - { code: ERROR_CODE.AUTH_REQUIRED }, - ); + throw new CliError(keylessCopy.billingNeedsClaimedApplication(), { + code: ERROR_CODE.AUTH_REQUIRED, + }); } } diff --git a/packages/cli-core/src/commands/config/keyless.ts b/packages/cli-core/src/commands/config/keyless.ts index 9ff12dac9..f05de4852 100644 --- a/packages/cli-core/src/commands/config/keyless.ts +++ b/packages/cli-core/src/commands/config/keyless.ts @@ -15,6 +15,7 @@ import { bapiRequest } from "../../lib/bapi.ts"; import { ERROR_CODE, throwUsageError, withApiContext } from "../../lib/errors.ts"; import type { KeylessTarget } from "../../lib/keyless-target.ts"; +import { keylessCopy } from "../../lib/copy.ts"; import { log } from "../../lib/log.ts"; /** @@ -120,18 +121,20 @@ function assertInstanceFields(fields: Record): void { if (unknown.length === 0) return; const lines = [ - `Unsupported ${unknown.length === 1 ? "field" : "fields"} on \`instance\` for an unclaimed keyless application: ${unknown.join(", ")}.`, - `Supported fields: ${[...INSTANCE_FIELDS].join(", ")}.`, + keylessCopy.unsupportedInstanceFieldsLine(unknown), + keylessCopy.supportedInstanceFieldsLine([...INSTANCE_FIELDS]), ]; // Say why, once per distinct reason, for the fields people actually try. const reasons = [ - ...new Set(unknown.map((field) => ACCOUNT_ONLY_INSTANCE_FIELDS[field]).filter(Boolean)), + ...new Set( + unknown + .map((field) => ACCOUNT_ONLY_INSTANCE_FIELDS[field]) + .filter((reason): reason is string => reason !== undefined), + ), ]; for (const reason of reasons) { - lines.push( - `Clerk's Backend API has no route for ${reason}, so this can't be changed from an unclaimed application at all — claim it first with \`clerk auth login\`.`, - ); + lines.push(keylessCopy.noRouteForInstanceFieldLine(reason)); } throwUsageError(lines.join("\n")); @@ -144,10 +147,7 @@ function assertInstanceFields(fields: Record): void { function asGroupNames(names: string[]): KeylessGroup[] { const unknown = names.filter((name) => !isGroupName(name)); if (unknown.length > 0) { - throwUsageError( - `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed keyless application: ${unknown.join(", ")}.\n` + - `Supported keys: ${KEYLESS_GROUP_NAMES.join(", ")}.`, - ); + throwUsageError(keylessCopy.unsupportedConfigKeys(unknown, KEYLESS_GROUP_NAMES)); } return names.filter(isGroupName); } @@ -162,23 +162,19 @@ export function assertKeylessPayload( const accountOnly = unknown.filter((key) => !API_REACHABLE_KEYLESS_KEYS.has(key)); const lines = [ - `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed keyless application: ${unknown.join(", ")}.`, - `Supported top-level keys: ${KEYLESS_GROUP_NAMES.join(", ")}.`, + keylessCopy.unsupportedPayloadKeysLine(unknown), + keylessCopy.supportedPayloadKeysLine(KEYLESS_GROUP_NAMES), ]; // Point these at `clerk api` — they're reachable today, and claiming the // application wouldn't move them into the config document anyway. if (apiReachable.length > 0) { - lines.push( - `${apiReachable.join(", ")} ${apiReachable.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use \`clerk api /${apiReachable[0]}\` directly instead of this config document.`, - ); + lines.push(keylessCopy.apiReachableKeysLine(apiReachable)); } // Everything left really is part of the account-mode config document. if (accountOnly.length > 0) { - lines.push( - "Run `clerk auth login` to claim the application and use the full config document.", - ); + lines.push(keylessCopy.claimForFullConfigLine()); } throwUsageError(lines.join("\n")); @@ -186,11 +182,7 @@ export function assertKeylessPayload( for (const [key, value] of Object.entries(payload)) { if (typeof value !== "object" || value === null || Array.isArray(value)) { - throwUsageError( - `Config key \`${key}\` must be a JSON object.`, - undefined, - ERROR_CODE.INVALID_JSON, - ); + throwUsageError(keylessCopy.configKeyMustBeObject(key), undefined, ERROR_CODE.INVALID_JSON); } } diff --git a/packages/cli-core/src/commands/config/push.ts b/packages/cli-core/src/commands/config/push.ts index 3c55d74d8..e60656191 100644 --- a/packages/cli-core/src/commands/config/push.ts +++ b/packages/cli-core/src/commands/config/push.ts @@ -11,6 +11,7 @@ import { confirm } from "../../lib/prompts.ts"; import { dim, bold, red, green } from "../../lib/color.ts"; import { withSpinner, intro, outro, pausedOutro } from "../../lib/spinner.ts"; import { isInsideGutter, log } from "../../lib/log.ts"; +import { keylessCopy } from "../../lib/copy.ts"; import { NEXT_STEPS, printNextSteps } from "../../lib/next-steps.ts"; import { resolveInstanceTarget } from "../../lib/keyless-target.ts"; import type { KeylessWriteVerification } from "./keyless.ts"; @@ -65,11 +66,9 @@ async function configPush(options: ConfigPushOptions, op: Operation): Promise // Same shape as config push/put: resolve the target once, branch on kind. const target = await resolveInstanceTarget(options); if (target.kind === "keyless") { - throw new CliError( - "Config schema is only available for a claimed application — the schema describes the account-level config document, which an unclaimed keyless application has no access to.\n" + - "Run `clerk auth login` to claim this application, then re-run `clerk config schema`.", - { code: ERROR_CODE.AUTH_REQUIRED }, - ); + throw new CliError(keylessCopy.schemaNeedsClaimedApplication(), { + code: ERROR_CODE.AUTH_REQUIRED, + }); } const ctx = target.ctx; diff --git a/packages/cli-core/src/commands/users/open.ts b/packages/cli-core/src/commands/users/open.ts index 7b959b5d3..cf8354e4b 100644 --- a/packages/cli-core/src/commands/users/open.ts +++ b/packages/cli-core/src/commands/users/open.ts @@ -2,6 +2,7 @@ import { bold, cyan, dim } from "../../lib/color.ts"; import { resolveAppContext, resolveInstanceId, resolveProfile } from "../../lib/config.ts"; import { CliError, ERROR_CODE, throwUsageError } from "../../lib/errors.ts"; import { resolveKeylessTarget } from "../../lib/keyless-target.ts"; +import { keylessCopy } from "../../lib/copy.ts"; import { log } from "../../lib/log.ts"; import { openBrowser } from "../../lib/open.ts"; import { intro, outro } from "../../lib/spinner.ts"; @@ -81,12 +82,9 @@ async function assertNotUnclaimedKeyless(options: UsersOpenOptions, userId: stri const keyless = await resolveKeylessTarget({ cwd: process.cwd() }); if (keyless) { - throw new CliError( - `This directory holds an unclaimed keyless application (secret key from ${keyless.source}), which has no Dashboard page — a dashboard link needs an application ID, and one is only assigned when the application is claimed.\n` + - `Run \`clerk auth login\` to claim it, then \`clerk users open ${userId}\` will work.\n` + - `To inspect the user right now, \`clerk api /users/${userId}\` reads it straight from the instance.`, - { code: ERROR_CODE.INSTANCE_NOT_FOUND }, - ); + throw new CliError(keylessCopy.userDashboardNeedsClaim(keyless.source, userId), { + code: ERROR_CODE.INSTANCE_NOT_FOUND, + }); } } diff --git a/packages/cli-core/src/lib/copy.ts b/packages/cli-core/src/lib/copy.ts new file mode 100644 index 000000000..cabf17aea --- /dev/null +++ b/packages/cli-core/src/lib/copy.ts @@ -0,0 +1,64 @@ +/** + * User-facing copy for the keyless surface, gathered in one module. + * + * One function per sentence (or self-contained message), with everything the + * sentence interpolates as typed parameters. Commands compose these lines but + * never author the prose inline — that is the contract that makes + * localisation possible later: a locale swaps this module's bodies without + * touching a single call site, and pluralisation/agreement rules live here + * where a translator can reach them instead of being scattered through + * command logic. + * + * House style every entry carries: backticks highlight commands and + * identifiers (`lib/log.ts` renders them cyan), and a refusal names the + * reason and a working alternative rather than just saying no. + */ +export const keylessCopy = { + // --- config payload validation (config/keyless.ts) --- + + unsupportedConfigKeys: (unknown: string[], supported: readonly string[]): string => + `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed keyless application: ${unknown.join(", ")}.\n` + + `Supported keys: ${supported.join(", ")}.`, + + unsupportedPayloadKeysLine: (unknown: string[]): string => + `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed keyless application: ${unknown.join(", ")}.`, + + supportedPayloadKeysLine: (supported: readonly string[]): string => + `Supported top-level keys: ${supported.join(", ")}.`, + + apiReachableKeysLine: (keys: string[]): string => + `${keys.join(", ")} ${keys.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use \`clerk api /${keys[0]}\` directly instead of this config document.`, + + claimForFullConfigLine: (): string => + "Run `clerk auth login` to claim the application and use the full config document.", + + configKeyMustBeObject: (key: string): string => `Config key \`${key}\` must be a JSON object.`, + + unsupportedInstanceFieldsLine: (unknown: string[]): string => + `Unsupported ${unknown.length === 1 ? "field" : "fields"} on \`instance\` for an unclaimed keyless application: ${unknown.join(", ")}.`, + + supportedInstanceFieldsLine: (fields: readonly string[]): string => + `Supported fields: ${fields.join(", ")}.`, + + noRouteForInstanceFieldLine: (reason: string): string => + `Clerk's Backend API has no route for ${reason}, so this can't be changed from an unclaimed application at all — claim it first with \`clerk auth login\`.`, + + // --- "needs a claimed application" refusals --- + + billingNeedsClaimedApplication: (): string => + "Billing can only be configured on a claimed application — Clerk's Backend API has no billing settings, so an unclaimed keyless application can't reach them.\n" + + "Run `clerk auth login` to claim this application, then re-run the command.", + + schemaNeedsClaimedApplication: (): string => + "Config schema is only available for a claimed application — the schema describes the account-level config document, which an unclaimed keyless application has no access to.\n" + + "Run `clerk auth login` to claim this application, then re-run `clerk config schema`.", + + putNeedsClaimedApplication: (): string => + "Replacing the entire configuration is only available for a claimed application — an unclaimed keyless application has no full config document to replace.\n" + + "Use `clerk config patch` to update individual settings, or run `clerk auth login` to claim the application first.", + + userDashboardNeedsClaim: (keySource: string, userId: string): string => + `This directory holds an unclaimed keyless application (secret key from ${keySource}), which has no Dashboard page — a dashboard link needs an application ID, and one is only assigned when the application is claimed.\n` + + `Run \`clerk auth login\` to claim it, then \`clerk users open ${userId}\` will work.\n` + + `To inspect the user right now, \`clerk api /users/${userId}\` reads it straight from the instance.`, +}; From 90e63bc866eeceb14ea462226832a01dca90846f Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Thu, 30 Jul 2026 20:29:16 -0300 Subject: [PATCH 08/10] docs(changeset): take the keyless-default release to major MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-out `clerk init` changes behaviour — it now mints a keyless application instead of launching the browser login — so the release is not purely additive even with CLERK_SECRET_KEY precedence restored. --- .changeset/keyless-mode-default.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/keyless-mode-default.md b/.changeset/keyless-mode-default.md index aa1f30e37..ef4e4d6b9 100644 --- a/.changeset/keyless-mode-default.md +++ b/.changeset/keyless-mode-default.md @@ -1,5 +1,5 @@ --- -"clerk": minor +"clerk": major --- Make keyless mode the default for unauthenticated `clerk init` runs on keyless-capable frameworks, and let an unclaimed keyless application be configured end to end without an account. From 99fd892e0bbbc418c2af9295235fb3175aee3549 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Tani Date: Fri, 31 Jul 2026 13:04:41 -0300 Subject: [PATCH 09/10] test(env): restore the iOS test's closing brace lost in a rebase conflict seam --- packages/cli-core/src/commands/env/pull.test.ts | 2 ++ packages/cli-core/src/commands/init/README.md | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/cli-core/src/commands/env/pull.test.ts b/packages/cli-core/src/commands/env/pull.test.ts index 8f69afff1..e5aa89f47 100644 --- a/packages/cli-core/src/commands/env/pull.test.ts +++ b/packages/cli-core/src/commands/env/pull.test.ts @@ -676,6 +676,8 @@ describe("env pull", () => { const content = await Bun.file(join(tempDir, ".env")).text(); expect(content).toContain("CLERK_PUBLISHABLE_KEY=pk_test_abc123"); expect(content).not.toContain("CLERK_SECRET_KEY"); + }); + describe("keyless", () => { // Encodes `.clerk.accounts.dev$` the way a real publishable key // would, so `decodePublishableKey` (used by the pairing check) sees the diff --git a/packages/cli-core/src/commands/init/README.md b/packages/cli-core/src/commands/init/README.md index c2312de3f..5da4c01b3 100644 --- a/packages/cli-core/src/commands/init/README.md +++ b/packages/cli-core/src/commands/init/README.md @@ -24,7 +24,7 @@ clerk init --no-skills | Option | Description | | ----------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| `--framework ` | Framework to set up (skips auto-detection). Valid values: `next`, `astro`, `nuxt`, `tanstack-start`, `react-router`, `vue`, `expo`, `react`, `javascript`, `js`, `express`, `fastify`, `ios`, `android` | +| `--framework ` | Framework to set up (skips auto-detection). Valid values: `next`, `astro`, `nuxt`, `tanstack-start`, `react-router`, `vue`, `expo`, `react`, `javascript`, `js`, `express`, `fastify`, `ios`, `android` | | `--pm ` | Package manager to use. Valid values: `bun`, `pnpm`, `yarn`, `npm`. Skips the PM prompt (bootstrap) or overrides lockfile detection (existing project) | | `--name ` | Project name for `--starter` (skips prompt). Must be lowercase, no spaces, no path separators | | `--app ` | Application ID to link (skips the interactive app picker during authenticated linking) | From 677ce20bcf4178b00c41eae783deb379fc0ded24 Mon Sep 17 00:00:00 2001 From: Rafael Thayto Date: Fri, 31 Jul 2026 09:18:33 -0300 Subject: [PATCH 10/10] fix(keyless): surface malformed local keys and exported key overrides MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolve the local secret key error in checkLoggedIn even when a stored account token exists, so a broken local CLERK_SECRET_KEY isn't hidden behind "Logged in" — other commands still prefer that key over the account session. whoami now also detects an exported CLERK_SECRET_KEY when a directory is linked, matching resolveBapiSecretKey's precedence: that env var overrides even a linked profile, so whoami's answer was wrong for the commands that honor it. Also list a `clerk api /` example for every key, not just the first, when telling the user which keys are already reachable on an unclaimed application. Addresses CodeRabbit review feedback on PR #395. --- .../cli-core/src/commands/doctor/checks.ts | 21 ++++++- .../src/commands/doctor/doctor.test.ts | 30 +++++++++- .../src/commands/whoami/index.test.ts | 55 +++++++++++++++++++ .../cli-core/src/commands/whoami/index.ts | 31 +++++++---- packages/cli-core/src/lib/copy.test.ts | 21 +++++++ packages/cli-core/src/lib/copy.ts | 2 +- 6 files changed, 145 insertions(+), 15 deletions(-) create mode 100644 packages/cli-core/src/lib/copy.test.ts diff --git a/packages/cli-core/src/commands/doctor/checks.ts b/packages/cli-core/src/commands/doctor/checks.ts index 5224051c3..6fa63ec35 100644 --- a/packages/cli-core/src/commands/doctor/checks.ts +++ b/packages/cli-core/src/commands/doctor/checks.ts @@ -95,11 +95,27 @@ async function claimHint(ctx: DoctorContext): Promise { export async function checkLoggedIn(ctx: DoctorContext): Promise { const check = defineCheck("Logged in", ctx.fixes.login); const token = await ctx.getToken(); - if (token) return check.pass("Logged in (token found in credential store)"); + + // Malformed-key detection is a side effect of resolving the keyless target + // (see getKeylessKeyError), so resolve it before any early return — a + // stored account token must not hide a broken local CLERK_SECRET_KEY that + // other commands still prefer over the account session. + const keyless = await ctx.getKeylessTarget(); + const keyError = await ctx.getKeylessKeyError(); + + if (token) { + if (keyError) { + return check.warn(`Logged in, but the local secret key is unusable: ${keyError.message}`, { + remedy: + "Fix or remove the malformed key — some commands prefer it over your account session.", + fixable: false, + }); + } + return check.pass("Logged in (token found in credential store)"); + } // No account session doesn't mean the project is broken: an unclaimed // keyless application is a legitimate, healthy way to run the CLI. - const keyless = await ctx.getKeylessTarget(); if (keyless) { const instance = await ctx.getKeylessInstance(); return check.pass( @@ -110,7 +126,6 @@ export async function checkLoggedIn(ctx: DoctorContext): Promise { // A local key that isn't a secret key at all is the one keyless state that // is genuinely broken — report it here as the named diagnosis, once, rather // than letting it crash every keyless-aware check (see getKeylessKeyError). - const keyError = await ctx.getKeylessKeyError(); if (keyError) { return check.fail(`Not logged in, and the local secret key is unusable: ${keyError.message}`, { remedy: "Fix or remove the malformed key, or run `clerk auth login` to authenticate.", diff --git a/packages/cli-core/src/commands/doctor/doctor.test.ts b/packages/cli-core/src/commands/doctor/doctor.test.ts index 89425cde4..60c213fb9 100644 --- a/packages/cli-core/src/commands/doctor/doctor.test.ts +++ b/packages/cli-core/src/commands/doctor/doctor.test.ts @@ -2,7 +2,7 @@ import { test, expect, describe, beforeEach, afterEach, mock } from "bun:test"; import { mkdtemp, rm } from "node:fs/promises"; import { join } from "node:path"; import { tmpdir } from "node:os"; -import { ApiError, AuthError, type CliError } from "../../lib/errors.ts"; +import { ApiError, AuthError, CliError, ERROR_CODE } from "../../lib/errors.ts"; import { _setConfigDir } from "../../lib/config.ts"; import { credentialStoreStubs, @@ -238,6 +238,34 @@ describe("checkLoggedIn", () => { message: ["unclaimed keyless application", "ins_keyless_1", "Claim it"], }); }); + + test("fail when no token and the local secret key is malformed", async () => { + const ctx = createMockContext({ + token: null, + keylessKeyError: new CliError("not a secret key", { code: ERROR_CODE.INVALID_KEY_FORMAT }), + }); + const result = await checkLoggedIn(ctx); + expectCheck(result, { + name: "Logged in", + status: "fail", + message: ["local secret key is unusable", "not a secret key"], + remedy: "Fix or remove the malformed key", + }); + }); + + test("warn when a stored token exists but the local secret key is malformed", async () => { + const ctx = createMockContext({ + token: "test_token", + keylessKeyError: new CliError("not a secret key", { code: ERROR_CODE.INVALID_KEY_FORMAT }), + }); + const result = await checkLoggedIn(ctx); + expectCheck(result, { + name: "Logged in", + status: "warn", + message: ["Logged in", "local secret key is unusable", "not a secret key"], + remedy: "Fix or remove the malformed key", + }); + }); }); describe("checkHostExecution", () => { diff --git a/packages/cli-core/src/commands/whoami/index.test.ts b/packages/cli-core/src/commands/whoami/index.test.ts index d5bbe9cb1..61fe89c88 100644 --- a/packages/cli-core/src/commands/whoami/index.test.ts +++ b/packages/cli-core/src/commands/whoami/index.test.ts @@ -14,6 +14,7 @@ const mockFetchUserInfo = mock(); const mockResolveProfile = mock(); const mockIsAgent = mock(); const mockResolveKeylessTarget = mock(); +const mockFindLocalSecretKey = mock(); const mockFindLocalPublishableKey = mock(); const mockHasKeyPairMismatch = mock(); const mockBapiRequest = mock(); @@ -27,6 +28,7 @@ mock.module("../../lib/credential-store.ts", () => ({ mock.module("../../lib/keyless-target.ts", () => ({ ...keylessTargetStubs, resolveKeylessTarget: (...args: unknown[]) => mockResolveKeylessTarget(...args), + findLocalSecretKey: (...args: unknown[]) => mockFindLocalSecretKey(...args), findLocalPublishableKey: (...args: unknown[]) => mockFindLocalPublishableKey(...args), hasKeyPairMismatch: (...args: unknown[]) => mockHasKeyPairMismatch(...args), })); @@ -74,8 +76,10 @@ describe("whoami", () => { mockResolveProfile.mockResolvedValue(undefined); mockHasStoredCredentials.mockResolvedValue(false); mockResolveKeylessTarget.mockResolvedValue(undefined); + mockFindLocalSecretKey.mockResolvedValue(undefined); mockFindLocalPublishableKey.mockResolvedValue(undefined); mockHasKeyPairMismatch.mockResolvedValue(false); + delete process.env.CLERK_SECRET_KEY; }); afterEach(() => { @@ -85,9 +89,11 @@ describe("whoami", () => { mockResolveProfile.mockReset(); mockIsAgent.mockReset(); mockResolveKeylessTarget.mockReset(); + mockFindLocalSecretKey.mockReset(); mockFindLocalPublishableKey.mockReset(); mockHasKeyPairMismatch.mockReset(); mockBapiRequest.mockReset(); + delete process.env.CLERK_SECRET_KEY; consoleSpy?.mockRestore(); }); @@ -175,6 +181,55 @@ describe("whoami", () => { expect(captured.err).not.toContain("Linked to"); }); + test("warns when unlinked and a local secret key is found", async () => { + mockGetValidToken.mockResolvedValue("valid-token"); + mockFetchUserInfo.mockResolvedValue({ userId: "user_123", email: "alice@example.com" }); + mockResolveProfile.mockResolvedValue(undefined); + mockFindLocalSecretKey.mockResolvedValue({ secretKey: "sk_test_xxx", source: ".env.local" }); + + await runWhoami(); + + expect(captured.err).toContain("isn't linked"); + expect(captured.err).toContain(".env.local"); + expect(captured.err).not.toContain("Linked to"); + }); + + test("warns when linked but an exported CLERK_SECRET_KEY overrides the profile", async () => { + mockGetValidToken.mockResolvedValue("valid-token"); + mockFetchUserInfo.mockResolvedValue({ userId: "user_123", email: "alice@example.com" }); + mockResolveProfile.mockResolvedValue(linkedProfile); + process.env.CLERK_SECRET_KEY = "sk_test_xxx"; + + await runWhoami(); + + expect(captured.err).toContain("Linked to"); + expect(captured.err).toContain("overrides the linked application"); + expect(captured.err).toContain("CLERK_SECRET_KEY env var"); + expect(mockFindLocalSecretKey).not.toHaveBeenCalled(); + }); + + test("no override warning when linked and no CLERK_SECRET_KEY is exported", async () => { + mockGetValidToken.mockResolvedValue("valid-token"); + mockFetchUserInfo.mockResolvedValue({ userId: "user_123", email: "alice@example.com" }); + mockResolveProfile.mockResolvedValue(linkedProfile); + + await runWhoami(); + + expect(captured.err).toContain("Linked to"); + expect(captured.err).not.toContain("overrides the linked application"); + }); + + test("--json reports the override source when linked but overridden", async () => { + mockGetValidToken.mockResolvedValue("valid-token"); + mockFetchUserInfo.mockResolvedValue({ userId: "user_123", email: "alice@example.com" }); + mockResolveProfile.mockResolvedValue(linkedProfile); + process.env.CLERK_SECRET_KEY = "sk_test_xxx"; + + await runWhoami({ json: true }); + + expect(JSON.parse(captured.out).localSecretKeySource).toBe("CLERK_SECRET_KEY env var"); + }); + test("--json emits structured payload with linked details and suppresses next-steps", async () => { mockGetValidToken.mockResolvedValue("valid-token"); mockFetchUserInfo.mockResolvedValue({ userId: "user_123", email: "alice@example.com" }); diff --git a/packages/cli-core/src/commands/whoami/index.ts b/packages/cli-core/src/commands/whoami/index.ts index 5dbe14233..25339a37b 100644 --- a/packages/cli-core/src/commands/whoami/index.ts +++ b/packages/cli-core/src/commands/whoami/index.ts @@ -31,11 +31,11 @@ type Identity = email: string; profile: Awaited>; /** - * Where a local secret key was found when the directory is unlinked, or - * null. It has to be surfaced here because the answer to "who am I?" is - * genuinely split in that state: this command reports the account, but - * `clerk api` and `clerk users` resolve the local key first and operate - * on whatever instance it addresses (see `resolveBapiSecretKey`). + * Where a local secret key was found that `clerk api` and `clerk users` + * would actually use instead of this account, or null. Populated when + * the directory is unlinked, and also when it's linked but an exported + * `CLERK_SECRET_KEY` overrides the profile — the one local-key source + * `resolveBapiSecretKey` still honors even then. */ localSecretKeySource: string | null; } @@ -102,8 +102,15 @@ async function resolveIdentity(): Promise { // Unlinked but holding a local secret key: the Backend API commands will use // that key, not this account, so whoami must say so or its answer is wrong - // for half the command surface. - const localKey = profile ? undefined : await findLocalSecretKey(process.cwd()); + // for half the command surface. A linked directory isn't automatically safe + // from this either — resolveBapiSecretKey lets an exported CLERK_SECRET_KEY + // win over even a linked profile, so check for that specific override + // instead of skipping discovery outright. + const localKey = profile + ? process.env.CLERK_SECRET_KEY + ? { source: "CLERK_SECRET_KEY env var" } + : undefined + : await findLocalSecretKey(process.cwd()); return { kind: "account", @@ -203,10 +210,14 @@ function render(identity: Identity): void { log.data(identity.email); if (identity.profile) { log.info(`Linked to \`${profileLabel(identity.profile.profile)}\``); - } else if (identity.localSecretKeySource) { + } + if (identity.localSecretKeySource) { log.warn( - `This directory isn't linked, but holds a secret key (from \`${identity.localSecretKeySource}\`) — \`clerk api\` and \`clerk users\` will use that key's instance, not your account.\n` + - "Run `clerk link` to point them at an application of yours, or remove the key.", + identity.profile + ? `An exported secret key (from \`${identity.localSecretKeySource}\`) overrides the linked application — \`clerk api\` and \`clerk users\` will use that key's instance instead.\n` + + "Unset `CLERK_SECRET_KEY` to use the linked application instead." + : `This directory isn't linked, but holds a secret key (from \`${identity.localSecretKeySource}\`) — \`clerk api\` and \`clerk users\` will use that key's instance, not your account.\n` + + "Run `clerk link` to point them at an application of yours, or remove the key.", ); } printNextSteps(identity.profile ? NEXT_STEPS.WHOAMI_LINKED : NEXT_STEPS.WHOAMI); diff --git a/packages/cli-core/src/lib/copy.test.ts b/packages/cli-core/src/lib/copy.test.ts new file mode 100644 index 000000000..2ab211408 --- /dev/null +++ b/packages/cli-core/src/lib/copy.test.ts @@ -0,0 +1,21 @@ +import { test, expect, describe } from "bun:test"; +import { keylessCopy } from "./copy.ts"; + +describe("apiReachableKeysLine", () => { + test.each([ + ["organization_settings", "`clerk api /organization_settings`"], + ["protect", "`clerk api /protect`"], + ])("names a single key's own clerk api example (%s)", (key, example) => { + const line = keylessCopy.apiReachableKeysLine([key]); + expect(line).toContain(`${key} is already reachable`); + expect(line).toContain(example); + }); + + test("names every key's own clerk api example when multiple keys are reachable", () => { + const line = keylessCopy.apiReachableKeysLine(["protect", "organization_settings"]); + expect(line).toContain("protect, organization_settings are already reachable"); + expect(line).toContain("`clerk api /protect`"); + expect(line).toContain("`clerk api /organization_settings`"); + expect(line).toContain(" or "); + }); +}); diff --git a/packages/cli-core/src/lib/copy.ts b/packages/cli-core/src/lib/copy.ts index cabf17aea..2e287b74f 100644 --- a/packages/cli-core/src/lib/copy.ts +++ b/packages/cli-core/src/lib/copy.ts @@ -27,7 +27,7 @@ export const keylessCopy = { `Supported top-level keys: ${supported.join(", ")}.`, apiReachableKeysLine: (keys: string[]): string => - `${keys.join(", ")} ${keys.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use \`clerk api /${keys[0]}\` directly instead of this config document.`, + `${keys.join(", ")} ${keys.length === 1 ? "is" : "are"} already reachable on an unclaimed application — use ${keys.map((key) => `\`clerk api /${key}\``).join(" or ")} directly instead of this config document.`, claimForFullConfigLine: (): string => "Run `clerk auth login` to claim the application and use the full config document.",