diff --git a/.changeset/quiet-lions-claim.md b/.changeset/quiet-lions-claim.md new file mode 100644 index 000000000..b0db51950 --- /dev/null +++ b/.changeset/quiet-lions-claim.md @@ -0,0 +1,5 @@ +--- +"clerk": minor +--- + +Add `clerk init --accountless` as the canonical flag and keep `--keyless` as a deprecated compatibility alias. `open` and `whoami` agent JSON now emit the canonical `accountless` key alongside the deprecated `keyless` alias. diff --git a/packages/cli-core/src/commands/api/README.md b/packages/cli-core/src/commands/api/README.md index 1ddb6b500..c9e8b1697 100644 --- a/packages/cli-core/src/commands/api/README.md +++ b/packages/cli-core/src/commands/api/README.md @@ -5,7 +5,7 @@ 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 +Works with no login and no linked project on an **unclaimed accountless 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. @@ -87,13 +87,13 @@ Secret key resolution order (Backend API, the default): 1. `--secret-key` flag (explicit) 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` +3. This project's own accountless 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) 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** — +`clerk api` work out of the box against an **unclaimed accountless 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 diff --git a/packages/cli-core/src/commands/auth/README.md b/packages/cli-core/src/commands/auth/README.md index 084fc4566..ced584a3d 100644 --- a/packages/cli-core/src/commands/auth/README.md +++ b/packages/cli-core/src/commands/auth/README.md @@ -6,7 +6,7 @@ Manage authentication with Clerk. ### `clerk auth login` (aliases: `signup`, `signin`, `sign-in`) -Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any keyless application previously created by `clerk init`. +Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or when an existing session is detected in agent mode), the command attempts to automatically claim any accountless application previously created by `clerk init`. 1. Checks for an existing valid token — if found, prompts to re-authenticate (in agent mode, skips and runs autoclaim immediately) 2. Generates PKCE parameters (code verifier, challenge, state) @@ -18,9 +18,9 @@ Authenticates the user via an OAuth 2.0 PKCE flow. After a successful login (or 8. If this was a re-authentication over an existing session, revokes the previous grant. The outgoing session is read once the authorization code arrives and just before the token exchange replaces it, and revoked only after the replacement is stored — so an abandoned browser flow leaves the original session intact, and a concurrent refresh has the smallest possible window to rotate the token out from under the revocation. A failure here warns rather than failing the login 9. **Autoclaim**: if `.clerk/keyless.json` exists in the current directory, claims the temporary application, links it to the project, and pulls environment variables -#### Keyless autoclaim breadcrumb lifecycle +#### Accountless autoclaim breadcrumb lifecycle -When `clerk init` runs in keyless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`: +When `clerk init` runs in accountless mode it writes `.clerk/keyless.json` containing a claim token. On the next `clerk auth login`: - **404** — claim token expired or application already deleted; breadcrumb is cleared and a warning is shown. - **403** — authenticated account has no active organization; breadcrumb is cleared and a warning is shown. @@ -37,7 +37,7 @@ OAuth requests are made against the Clerk OAuth system instance (default `https: | Token exchange | `POST` | `/oauth/token` | Exchanges authorization code + `code_verifier` for an access token | | User info | `GET` | `/oauth/userinfo` | Fetches `sub` (user ID) and `email` using the access token | | Revoke | `POST` | `/oauth/token/revoke` | Revokes the superseded refresh token on re-authentication (RFC 7009) | -| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims a keyless application by token; returns the full `Application` object | +| Autoclaim | `POST` | `/v1/platform/accountless_applications/claim` | Claims an accountless application by token; returns the full `Application` object | ### `clerk auth logout` (aliases: `signout`, `sign-out`) diff --git a/packages/cli-core/src/commands/billing/README.md b/packages/cli-core/src/commands/billing/README.md index 2b8c00d53..4696ef23f 100644 --- a/packages/cli-core/src/commands/billing/README.md +++ b/packages/cli-core/src/commands/billing/README.md @@ -5,9 +5,9 @@ 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 +commands cannot run against an unclaimed accountless 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 +no billing resource an instance secret key could reach. In an accountless project both commands exit with an `auth_required` error pointing at `clerk auth login`. For arbitrary billing config edits (plans, trials, payment-method requirements) diff --git a/packages/cli-core/src/commands/config/README.md b/packages/cli-core/src/commands/config/README.md index 3a40d3d46..dc63d05e9 100644 --- a/packages/cli-core/src/commands/config/README.md +++ b/packages/cli-core/src/commands/config/README.md @@ -5,7 +5,7 @@ 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). +- **Accountless 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 [Accountless mode](#accountless-mode). ## Commands @@ -36,7 +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 +- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account #### API Endpoints @@ -75,7 +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)) +- 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 [accountless mode](#accountless-mode)) #### API Endpoints @@ -116,7 +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 +- **Or neither**: an unlinked project holding an instance secret key falls back to [accountless mode](#accountless-mode), which needs no account #### API Endpoints @@ -157,7 +157,7 @@ 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)) +- Account-only: in an unlinked project holding an instance secret key this exits with an error pointing at `clerk config patch` (see [accountless mode](#accountless-mode)) #### API Endpoints @@ -167,9 +167,9 @@ clerk config put --file full-config.json --dry-run --- -## Keyless mode +## Accountless 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. +An accountless 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 @@ -178,11 +178,11 @@ 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. +**Account credentials are not part of this decision.** Accountless 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`): +The secret key is resolved the way the app itself would resolve one, and this order is shared by every accountless-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 @@ -192,7 +192,7 @@ A key that doesn't start with `sk_` is rejected. The SDK file comes last because ### 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: +The Backend API has no single config document — it exposes independent resources — so accountless 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 '{ @@ -221,19 +221,19 @@ Any other top-level key exits with a usage error naming the supported ones. Most ### 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. +A 200 or 204 from an accountless 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. +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 the legacy-named `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 | +| Behavior | Account mode | Accountless mode | | ------------------ | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | Coverage | Full config document | Seven Backend API resources | | `--instance` | Selects dev/prod | Usage error — the secret key already targets exactly one instance | @@ -244,7 +244,7 @@ That leaves `PATCH /v1/instance`, which answers `204` with no body at all. Nothi 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) +### API Endpoints (accountless 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. diff --git a/packages/cli-core/src/commands/config/keyless.test.ts b/packages/cli-core/src/commands/config/keyless.test.ts index 6ae707159..497de6991 100644 --- a/packages/cli-core/src/commands/config/keyless.test.ts +++ b/packages/cli-core/src/commands/config/keyless.test.ts @@ -285,7 +285,7 @@ describe("keyless config", () => { 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/, + /--instance is not supported for an unclaimed accountless application/, ); }); @@ -761,7 +761,7 @@ describe("keyless config", () => { 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"); + expect(captured.err).toContain("Accountless application keys"); }); test("enable billing explains that billing needs a claimed application", async () => { diff --git a/packages/cli-core/src/commands/doctor/README.md b/packages/cli-core/src/commands/doctor/README.md index 443043cc7..a105a908e 100644 --- a/packages/cli-core/src/commands/doctor/README.md +++ b/packages/cli-core/src/commands/doctor/README.md @@ -37,32 +37,32 @@ 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 +### Accountless 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`): +the same accountless 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 +**unclaimed accountless application** — a legitimate, healthy state, not a broken one. -- No token, keyless key present → **pass**, naming the instance. The claim +- No token, accountless 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 +- Stored session expired, accountless key present → **warn** (not fail): the + accountless key still works, logging in again is optional. +- Signed in (has account credentials) but this directory isn't linked, accountless 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 +- No token **and** no accountless key found anywhere → still **fail**. Accountless 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 +API application/instance-list concepts have no accountless equivalent), so they +continue to skip for an accountless project — the skip reason names the accountless application instead of reading like a problem. ## Auto-Fix (`--fix`) @@ -117,8 +117,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 | -| `GET` | `/v1/instance` | Names the keyless application (best-effort, via its secret key) | +| 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 accountless 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 a7cf51c62..be54306b9 100644 --- a/packages/cli-core/src/commands/doctor/checks.ts +++ b/packages/cli-core/src/commands/doctor/checks.ts @@ -67,7 +67,7 @@ function defineCheck(name: string, fixFactory?: () => FixAction): CheckBuilder { }; } -/** How to refer to an unclaimed keyless application in check output. */ +/** How to refer to an unclaimed accountless 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})` : ""; @@ -114,11 +114,11 @@ export async function checkLoggedIn(ctx: DoctorContext): Promise { } // No account session doesn't mean the project is broken: an unclaimed - // keyless application is a legitimate, healthy way to run the CLI. + // accountless application is a legitimate, healthy way to run the CLI. if (keyless) { const instance = await ctx.getKeylessInstance(); return check.pass( - `Not logged in — running on the unclaimed keyless application ${keylessLabel(keyless, instance)}. ${await claimHint(ctx)}`, + `Not logged in — running on the unclaimed accountless application ${keylessLabel(keyless, instance)}. ${await claimHint(ctx)}`, ); } @@ -162,7 +162,7 @@ export async function checkTokenValid(ctx: DoctorContext): Promise if (!storedToken) { const keyless = await ctx.getKeylessTarget(); return keyless - ? check.pass("No account session — not required for this keyless application") + ? check.pass("No account session — not required for this accountless application") : check.skip("no token"); } @@ -179,10 +179,10 @@ export async function checkTokenValid(ctx: DoctorContext): Promise if (keyless) { const instance = await ctx.getKeylessInstance(); return check.warn( - `Stored session is expired — falling back to the keyless application ${keylessLabel(keyless, instance)}`, + `Stored session is expired — falling back to the accountless application ${keylessLabel(keyless, instance)}`, { remedy: - "Run `clerk auth login` to re-authenticate your account (optional for keyless work).", + "Run `clerk auth login` to re-authenticate your account (optional for accountless work).", fixable: false, }, ); @@ -221,7 +221,7 @@ export async function checkProjectLinked(ctx: DoctorContext): Promise { // 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", + keyless ? "accountless application, no linked instances to verify" : "not authenticated", ); } diff --git a/packages/cli-core/src/commands/doctor/context.ts b/packages/cli-core/src/commands/doctor/context.ts index 0e82432cc..9c18a30b5 100644 --- a/packages/cli-core/src/commands/doctor/context.ts +++ b/packages/cli-core/src/commands/doctor/context.ts @@ -103,7 +103,7 @@ export function createDoctorContext(): DoctorContext { } 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)})`); + log.debug(`doctor: could not fetch accountless instance info (${errorMessage(error)})`); return null; } })(); diff --git a/packages/cli-core/src/commands/doctor/doctor.test.ts b/packages/cli-core/src/commands/doctor/doctor.test.ts index 60c213fb9..b8022f10e 100644 --- a/packages/cli-core/src/commands/doctor/doctor.test.ts +++ b/packages/cli-core/src/commands/doctor/doctor.test.ts @@ -235,7 +235,7 @@ describe("checkLoggedIn", () => { 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"], + message: ["unclaimed accountless application", "ins_keyless_1", "Claim it"], }); }); @@ -392,7 +392,7 @@ describe("checkTokenValid", () => { expectCheck(result, { name: "Authentication valid", status: "warn", - message: ["Stored session is expired", "keyless application"], + message: ["Stored session is expired", "accountless application"], remedy: "clerk auth login", fix: false, }); @@ -435,7 +435,7 @@ describe("checkProjectLinked", () => { 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"], + message: ["unclaimed accountless application", "ins_keyless_1", "Claim it"], }); }); @@ -450,7 +450,7 @@ describe("checkProjectLinked", () => { expectCheck(result, { name: "Project linked", status: "warn", - message: ["keyless application", "fewer settings"], + message: ["accountless application", "fewer settings"], remedy: "clerk link", fix: true, }); @@ -522,7 +522,7 @@ describe("checkLinkedAppExists", () => { expectCheck(result, { name: "Application reachable", status: "warn", - message: ["Skipped", "keyless application"], + message: ["Skipped", "accountless application"], }); }); }); @@ -601,7 +601,7 @@ describe("checkInstances", () => { expectCheck(result, { name: "Instance IDs", status: "warn", - message: ["Skipped", "keyless application"], + message: ["Skipped", "accountless application"], }); }); }); diff --git a/packages/cli-core/src/commands/env/README.md b/packages/cli-core/src/commands/env/README.md index af3a08183..e85a88dd9 100644 --- a/packages/cli-core/src/commands/env/README.md +++ b/packages/cli-core/src/commands/env/README.md @@ -2,9 +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. 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 **accountless** 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 accountless 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)). +The secret key and publishable key are found independently and can each belong to a _different_ application (e.g. leftovers from two accountless 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 @@ -44,7 +44,7 @@ sequenceDiagram CLI->>API: GET /v1/platform/applications/{appId} 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) + else Unclaimed accountless 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 @@ -82,11 +82,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 | -| Verify keyless pairing (keyless path only) | `GET` | `/v1/domains` | Only when a local publishable key was found; authenticated with the secret key | +| 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 accountless pairing (accountless 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.ts b/packages/cli-core/src/commands/env/pull.ts index 62b977fbd..165e1c7d3 100644 --- a/packages/cli-core/src/commands/env/pull.ts +++ b/packages/cli-core/src/commands/env/pull.ts @@ -117,7 +117,7 @@ async function mergeKeysIntoEnvFile( } /** - * Writes a keyless application's local keys into the project's env file. The + * Writes an accountless 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. */ @@ -141,7 +141,7 @@ async function pullKeylessKeys( if (publishableKey) { const mismatch = await withApiContext( hasKeyPairMismatch(keyless, publishableKey), - `Failed to verify the keyless secret key from \`${keyless.source}\``, + `Failed to verify the accountless secret key from \`${keyless.source}\``, ); if (mismatch) { throw new CliError( @@ -160,7 +160,7 @@ async function pullKeylessKeys( ...(publishableKey && { [publishableKeyName]: publishableKey }), }); - log.info(`Keyless application keys from \`${keyless.source}\` written to ${displayPath}`); + log.info(`Accountless 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 5da4c01b3..de2eb7cf3 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. 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. +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 accountless, init defaults to accountless 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. `--accountless` forces accountless (even when logged in); `--login` forces the authenticated flow. ## Usage @@ -11,10 +11,10 @@ clerk init --framework next 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 --starter --framework next --accountless clerk init --login clerk init --template b2b-saas -clerk init --keyless --fresh +clerk init --accountless --fresh clerk init -y clerk init --yes clerk init --no-skills @@ -22,19 +22,21 @@ 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`. 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 | +| 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) | +| `--accountless` | Force auto-generated temporary development keys, even when logged in. Only valid on an accountless-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 accountless keys. Errors in agent mode when unauthenticated (agents can't run OAuth) | +| `--template ` | Pre-configure the accountless application at creation: `b2b-saas`, `b2c-saas`, `native`, `waitlist`. Only applies when the run resolves to accountless — errors otherwise (see [Application templates](#application-templates)); cannot be combined with `--login` | +| `--fresh` | Replace an existing unclaimed accountless application with a new one, instead of keeping it (see [Accountless breadcrumb](#accountless-breadcrumb)). Only applies when the run resolves to accountless — errors otherwise; cannot be combined with `--login` | +| `-y, --yes` | Skip y/n confirmation prompts only. It neither forces nor bypasses accountless — the strategy is picked by auth state, mode, and flags. It does **not** replace an existing unclaimed accountless app — that still requires `--fresh` | +| `--no-skills` | Skip the optional agent skills install prompt at the end of init | + +`--keyless` remains accepted as a deprecated, hidden compatibility alias for `--accountless`. Use `--accountless` in all new commands and documentation. ## Agent Mode @@ -44,27 +46,27 @@ When running in agent mode (`--mode agent` or non-TTY), the command runs the ful - For **existing projects**: framework and package manager are auto-detected, no flags required - For **new projects** (`--starter` or blank directory): `--framework` is required (no way to auto-detect in an empty dir). Package manager is auto-selected by availability (bun → pnpm → yarn → npm) unless `--pm` is provided - 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: +- For accountless-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 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. + - When **unauthenticated**, init uses accountless: the app runs on auto-generated dev keys, and init writes a legacy-named `.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) +- 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 an accountless-capable framework to accountless 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 accountless application over an existing unclaimed one on re-run — see [Accountless breadcrumb](#accountless-breadcrumb) ## Flow 1. Gathers project context (framework, router variant, TypeScript, `src/` directory, package manager) 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 + - **`--accountless`**: forces accountless mode, even when logged in. Only valid on an accountless-capable framework, and cannot be combined with `--login` or `--app` (usage errors otherwise). The app runs on auto-generated dev keys; init writes a legacy-named `.clerk/keyless.json` breadcrumb so the next `clerk auth login` claims the app automatically - **`--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. 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 + - **Agent + non-accountless framework + no real app target**: scaffolds locally and prints manual setup instructions instead of selecting or creating an app + - **Agent + accountless-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 + accountless-capable framework + unauthenticated + no real app target**: uses accountless 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 + accountless-capable framework + not authenticated**: uses accountless 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) + - `--template` and `--fresh` are rejected with a usage error whenever the resolved strategy above isn't accountless — see [Application templates](#application-templates) and [Accountless breadcrumb](#accountless-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 @@ -77,26 +79,26 @@ 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. **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 +15. **Accountless mode** (unauthenticated runs whose resolved strategy in step 2 is accountless — an unauthenticated human-mode rerun on an existing project resolves to the authenticated flow instead): mints an accountless application and prints instructions for development without API keys and how to connect a Clerk account later — unless an unclaimed accountless app already exists for this project (see [Re-running init on an already-accountless project](#re-running-init-on-an-already-accountless-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 Detects the project's framework from `package.json` dependencies (checked top-to-bottom, first match wins): -| Dependency | Framework | Clerk SDK | Publishable Key Env Var | Keyless | -| ----------------------- | -------------- | ----------------------------- | ----------------------------------- | ------- | -| `next` | Next.js | `@clerk/nextjs` | `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Yes | -| `astro` | Astro | `@clerk/astro` | `PUBLIC_CLERK_PUBLISHABLE_KEY` | Yes | -| `nuxt` | Nuxt | `@clerk/nuxt` | `NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Yes | -| `@tanstack/react-start` | TanStack Start | `@clerk/tanstack-react-start` | `VITE_CLERK_PUBLISHABLE_KEY` | Yes | -| `react-router` | React Router | `@clerk/react-router` | `VITE_CLERK_PUBLISHABLE_KEY` | Yes | -| `vue` | Vue | `@clerk/vue` | `VITE_CLERK_PUBLISHABLE_KEY` | No | -| `expo` | Expo | `@clerk/expo` | `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` | No | -| `react` | React | `@clerk/react` | `VITE_CLERK_PUBLISHABLE_KEY` | No | -| `vite` | JavaScript | `@clerk/clerk-js` | `VITE_CLERK_PUBLISHABLE_KEY` | No | -| `express` | Express | `@clerk/express` | `CLERK_PUBLISHABLE_KEY` | No | -| `fastify` | Fastify | `@clerk/fastify` | `CLERK_PUBLISHABLE_KEY` | No | +| Dependency | Framework | Clerk SDK | Publishable Key Env Var | Accountless | +| ----------------------- | -------------- | ----------------------------- | ----------------------------------- | ----------- | +| `next` | Next.js | `@clerk/nextjs` | `NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Yes | +| `astro` | Astro | `@clerk/astro` | `PUBLIC_CLERK_PUBLISHABLE_KEY` | Yes | +| `nuxt` | Nuxt | `@clerk/nuxt` | `NUXT_PUBLIC_CLERK_PUBLISHABLE_KEY` | Yes | +| `@tanstack/react-start` | TanStack Start | `@clerk/tanstack-react-start` | `VITE_CLERK_PUBLISHABLE_KEY` | Yes | +| `react-router` | React Router | `@clerk/react-router` | `VITE_CLERK_PUBLISHABLE_KEY` | Yes | +| `vue` | Vue | `@clerk/vue` | `VITE_CLERK_PUBLISHABLE_KEY` | No | +| `expo` | Expo | `@clerk/expo` | `EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY` | No | +| `react` | React | `@clerk/react` | `VITE_CLERK_PUBLISHABLE_KEY` | No | +| `vite` | JavaScript | `@clerk/clerk-js` | `VITE_CLERK_PUBLISHABLE_KEY` | No | +| `express` | Express | `@clerk/express` | `CLERK_PUBLISHABLE_KEY` | No | +| `fastify` | Fastify | `@clerk/fastify` | `CLERK_PUBLISHABLE_KEY` | No | Native mobile platforms may not have a `package.json`, so they are detected from project marker files when no npm framework matches: @@ -107,7 +109,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 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. +The **Accountless** column indicates whether the framework's Clerk SDK supports accountless mode (auto-generated temporary dev keys). Accountless 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. `--accountless` forces accountless 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 an accountless-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. @@ -236,7 +238,7 @@ No files are written. The scaffold plan prints the quickstart steps: SDK install ## Agent skills install -After scaffolding (and after env keys are pulled or keyless instructions are printed), `clerk init` offers to install Clerk's agent skills via the [`skills`](https://www.npmjs.com/package/skills) CLI. The runner is detected from the project's package manager (`bunx`, `npx`, `pnpm dlx`, or `yarn dlx`), so a Bun project installs via `bunx skills add ...`, a pnpm project via `pnpm dlx skills add ...`, and so on. This step is optional and non-fatal: if no package runner is available on PATH or an install command exits non-zero, init prints a yellow warning with a runner-appropriate manual command and still exits successfully. +After scaffolding (and after env keys are pulled or accountless instructions are printed), `clerk init` offers to install Clerk's agent skills via the [`skills`](https://www.npmjs.com/package/skills) CLI. The runner is detected from the project's package manager (`bunx`, `npx`, `pnpm dlx`, or `yarn dlx`), so a Bun project installs via `bunx skills add ...`, a pnpm project via `pnpm dlx skills add ...`, and so on. This step is optional and non-fatal: if no package runner is available on PATH or an install command exits non-zero, init prints a yellow warning with a runner-appropriate manual command and still exits successfully. - **Human mode**: prompts `Install agent skills? (...)` defaulting to yes. Pass `--no-skills` to suppress the prompt entirely, or `-y/--yes` to accept it without confirmation. When more than one runner is available, a second prompt picks which one to use (the project's package manager wins by default). - **Agent mode**: skills are installed non-interactively with `-y -g` flags (no prompt shown). Pass `--no-skills` to skip entirely. @@ -272,15 +274,15 @@ Implementation lives in [`skills.ts`](./skills.ts). Note that the E2E fixture se ## API Endpoints -| Step | Method | Base URL | Endpoint | Description | -| ---------------------- | ------ | ------------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------- | -| Create accountless app | `POST` | `CLERK_BAPI_URL` (default BAPI) | `/v1/accountless_applications` | Creates a temporary keyless Clerk application; returns `publishable_key`, `secret_key`, and `claim_url`. Only called in keyless mode. | +| Step | Method | Base URL | Endpoint | Description | +| ---------------------- | ------ | ------------------------------- | ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- | +| Create accountless app | `POST` | `CLERK_BAPI_URL` (default BAPI) | `/v1/accountless_applications` | Creates a temporary accountless Clerk application; returns `publishable_key`, `secret_key`, and `claim_url`. Only called in accountless mode. | 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 ` 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` accountless app comes back with organizations already enabled, where a default one does not. | Template | Shape | | ---------- | -------------------------------- | @@ -289,11 +291,11 @@ 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 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)). +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 accountless — 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 accountless at all. The error names the reason, so `--template` is never silently dropped: add `--accountless` to force an accountless app, or drop `--template`. Settings can still be changed afterwards with `clerk config patch`, which also works without an account (see [config accountless mode](../config/README.md#accountless-mode)). -## Keyless breadcrumb +## Accountless 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. +In accountless mode, after calling `POST /v1/accountless_applications`, `clerk init` writes the legacy-named `.clerk/keyless.json` breadcrumb to the project root. The filename remains unchanged so older CLI versions can still claim the application. 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. ```json { @@ -304,12 +306,12 @@ 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 +### Re-running init on an already-accountless 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. 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: +The breadcrumb is also what protects an unclaimed accountless 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 accountless 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** (no `-y`): prompts `This project already has an unclaimed accountless 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. +- **`--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 accountless. 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 6b22e10c2..dff1e956f 100644 --- a/packages/cli-core/src/commands/init/heuristics.ts +++ b/packages/cli-core/src/commands/init/heuristics.ts @@ -160,12 +160,12 @@ export function printKeylessInfo(envFile: string): void { /** * Printed instead of `printKeylessInfo` when init keeps an existing unclaimed - * keyless app rather than minting a replacement (see `shouldKeepExistingKeyless`). + * accountless 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`, + `\n This project already has an unclaimed accountless application (keys in ${envFile}).`, + ` Run ${bold("clerk auth login")} to claim it, or ${bold("clerk init --accountless --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.test.ts b/packages/cli-core/src/commands/init/index.test.ts index 55b9c7099..e45a102a5 100644 --- a/packages/cli-core/src/commands/init/index.test.ts +++ b/packages/cli-core/src/commands/init/index.test.ts @@ -623,7 +623,7 @@ describe("init", () => { setup({ email: "test@test.com" }); const stages = trackStages(); - await expect(init({ keyless: true, login: true })).rejects.toThrow(); + await expect(init({ accountless: true, login: true })).rejects.toThrow(); expect(stages()).toEqual(["flags"]); }); diff --git a/packages/cli-core/src/commands/init/index.ts b/packages/cli-core/src/commands/init/index.ts index 5f14811fb..10f8e7ef1 100644 --- a/packages/cli-core/src/commands/init/index.ts +++ b/packages/cli-core/src/commands/init/index.ts @@ -74,13 +74,15 @@ type InitOptions = { starter?: boolean; /** Link to a specific Clerk application by ID (skips the interactive picker). */ app?: string; - /** Force keyless mode (auto-generated dev keys, no login). Only valid on keyless-capable frameworks. */ + /** Force accountless setup (auto-generated dev keys, no login). */ + accountless?: boolean; + /** Deprecated alias for `accountless`. */ keyless?: boolean; - /** Force the authenticated flow (log in and link a real app) instead of defaulting to keyless. */ + /** Force the authenticated flow (log in and link a real app) instead of defaulting to accountless. */ login?: boolean; - /** Pre-configure the keyless application from a Clerk application template. */ + /** Pre-configure the accountless application from a Clerk application template. */ template?: KeylessTemplate; - /** Replace an existing unclaimed keyless application instead of keeping it. */ + /** Replace an existing unclaimed accountless application instead of keeping it. */ fresh?: boolean; }; @@ -89,7 +91,11 @@ export async function init(options: InitOptions = {}) { const agent = isAgent(); setTelemetryStage("flags"); - await assertUsableFlags(options, agent); + const optsAccountless = options.accountless === true || options.keyless === true; + if (options.keyless) { + log.warn("`--keyless` is deprecated. Use `--accountless` instead."); + } + await assertUsableFlags(options, agent, optsAccountless); const frameworkOverride = options.framework ? (lookupFramework(options.framework) ?? undefined) @@ -119,8 +125,7 @@ export async function init(options: InitOptions = {}) { await enrichProjectContext(ctx); - const optsKeyless = options.keyless === true; - // Skip auth-related I/O entirely when the user opted into keyless — those + // Skip auth-related I/O entirely when the user opted into accountless setup — those // values are not consumed once the strategy resolves to "keyless". // // Agent mode has no way to recover if this lies: a human who turns out to be @@ -129,17 +134,17 @@ export async function init(options: InitOptions = {}) { // round-trip it can never complete. So agent mode validates the credential // (it can fall back to keyless) instead of trusting mere presence. setTelemetryStage("strategy"); - const authed = optsKeyless + const authed = optsAccountless ? false : agent ? await isAuthenticatedForAgent() : await isAuthenticated(); const linkedProfile = - !optsKeyless && agent && !options.app ? await resolveProfile(ctx.cwd) : undefined; + !optsAccountless && agent && !options.app ? await resolveProfile(ctx.cwd) : undefined; const hasRealAppTarget = Boolean(options.app || linkedProfile); const strategy = pickStrategy({ - optsKeyless, + optsAccountless, optsLogin: options.login === true, agent, authed, @@ -148,7 +153,7 @@ export async function init(options: InitOptions = {}) { framework: ctx.framework, }); - assertKeylessOnlyFlags(options, strategy); + assertKeylessOnlyFlags(options, strategy, Boolean(ctx.framework.supportsKeyless)); if (strategy === "authenticate") { setTelemetryStage("link"); @@ -203,26 +208,32 @@ export async function init(options: InitOptions = {}) { /** * Rejects flag combinations that can't both be honoured, before anything is - * bootstrapped on disk. `--keyless`, `--template`, and `--fresh` describe an + * bootstrapped on disk. `--accountless`, `--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) { - throwUsageError("--keyless and --login cannot be combined."); +async function assertUsableFlags( + options: InitOptions, + agent: boolean, + accountless: boolean, +): Promise { + if (accountless && options.login) { + throwUsageError("--accountless and --login cannot be combined."); } - if (options.keyless && options.app) { + if (accountless && options.app) { throwUsageError( - "--keyless cannot be combined with --app. Drop --keyless to link the app, or drop --app to use temporary development keys.", + "--accountless cannot be combined with --app. Drop --accountless 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.", + "--template applies to accountless applications and cannot be combined with --login.", ); } if (options.fresh && options.login) { - throwUsageError("--fresh applies to keyless applications and cannot be combined with --login."); + throwUsageError( + "--fresh applies to accountless 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 @@ -250,30 +261,56 @@ async function isAuthenticatedForAgent(): Promise { } /** - * `--template` and `--fresh` only take effect when init creates a keyless + * `--template` and `--fresh` only take effect when init creates an accountless * 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 + * same way `--accountless`+`--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 { +function assertKeylessOnlyFlags( + options: InitOptions, + strategy: InitStrategy, + supportsAccountless: boolean, +): 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)"; + // "Add --accountless" is only valid remediation when accountless setup is + // actually reachable from here — not when the framework doesn't support it + // or when --app/--login are what forced the authenticated flow (both + // conflict with --accountless in assertUsableFlags above). Framework + // support is checked directly, not via strategy: an unsupported framework + // resolves to "manual" only in agent mode — in human mode it resolves to + // "authenticate", which would otherwise suggest an --accountless flag the + // framework rejects. + let reason: string; + // Null when dropping the offending flag is the only remediation. + let remedy: string | null; + if (!supportsAccountless) { + reason = "this framework does not support accountless setup"; + remedy = null; + } else if (options.app) { + reason = "--app was set, which cannot be combined with --accountless"; + remedy = "drop --app to allow accountless setup"; + } else if (options.login) { + reason = "--login was set, which cannot be combined with --accountless"; + remedy = "drop --login to allow accountless setup"; + } else { + reason = + "this run resolved to the authenticated flow instead (already signed in, or a project is already linked)"; + remedy = "add --accountless to force an accountless app"; + } + const tail = (flag: string): string => (remedy ? `${remedy}, or drop ${flag}.` : `drop ${flag}.`); if (options.template) { throwUsageError( - `--template only applies to keyless applications, but ${reason}. Add --keyless to force a keyless app, or drop --template.`, + `--template only applies to accountless applications, but ${reason}; ${tail("--template")}`, ); } if (options.fresh) { throwUsageError( - `--fresh only applies to keyless applications, but ${reason}. Add --keyless to force a keyless app, or drop --fresh.`, + `--fresh only applies to accountless applications, but ${reason}; ${tail("--fresh")}`, ); } } @@ -357,17 +394,17 @@ function devCommand(pm: string): string { function printBootstrapNextSteps( { projectName, packageManager }: BootstrapResult, - keyless: boolean, + accountless: boolean, ): void { const steps = [`cd ${projectName}`, devCommand(packageManager)]; - if (keyless) { + if (accountless) { steps.push("clerk auth login (when you're ready to connect your Clerk account)"); } printNextSteps(steps); } function printBootstrapManualSetupInfo(framework: FrameworkInfo): void { - // Only reachable for non-keyless frameworks: keyless-capable ones resolve to + // Only reachable for frameworks without accountless support: capable ones resolve to // the "keyless" or "authenticate" strategy in agent mode instead. const lines = [ `\n Set up Clerk for ${framework.name}:`, @@ -383,16 +420,16 @@ function printBootstrapManualSetupInfo(framework: FrameworkInfo): void { type InitStrategy = "keyless" | "manual" | "authenticate"; // Picks how `clerk init` will reach a working Clerk setup: -// - "keyless" → temporary development keys, no login. Forced via `--keyless`, or the default +// - "keyless" → temporary development keys, no login. Forced via `--accountless`, 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 +// all agent runs). A legacy `.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. +// via `--login`, and the default whenever accountless setup doesn't apply. function pickStrategy({ - optsKeyless, + optsAccountless, optsLogin, agent, authed, @@ -400,7 +437,7 @@ function pickStrategy({ hasRealAppTarget, framework, }: { - optsKeyless: boolean; + optsAccountless: boolean; optsLogin: boolean; agent: boolean; authed: boolean; @@ -408,10 +445,10 @@ function pickStrategy({ hasRealAppTarget: boolean; framework: FrameworkInfo; }): InitStrategy { - if (optsKeyless) { + if (optsAccountless) { if (!framework.supportsKeyless) { throwUsageError( - `--keyless is not supported for ${framework.name}. Run \`clerk auth login\` and use \`clerk init --app \` instead.`, + `--accountless is not supported for ${framework.name}. Run \`clerk auth login\` and use \`clerk init --app \` instead.`, ); } return "keyless"; @@ -486,8 +523,8 @@ 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 the + * A legacy `.clerk/keyless.json` breadcrumb means an earlier run already minted an + * unclaimed accountless 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` @@ -513,8 +550,8 @@ async function shouldKeepExistingKeyless( const replace = await confirm({ 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?", + ? `This project already has an unclaimed accountless application (created ${existing.createdAt}). Replace it with a new one?` + : "This project already has an unclaimed accountless application (minted by its Clerk SDK in `.clerk/.tmp/keyless.json`). Replace it with a new one?", default: false, }); return !replace; @@ -656,22 +693,23 @@ export function registerInit(program: Program): void { .option("--app ", "Application ID to link (skips interactive picker)") .option("--starter", "Create a new project from a starter template") .option( - "--keyless", - "Force keyless development keys, even when logged in (only for keyless-capable frameworks)", + "--accountless", + "Force accountless development keys, even when logged in (only for supported frameworks)", ) + .addOption(createOption("--keyless", "Deprecated alias for --accountless").hideHelp()) .option( "--login", - "Force the authenticated flow: log in and link a real application instead of keyless keys", + "Force the authenticated flow: log in and link a real application instead of accountless keys", ) .addOption( createOption( "--template ", - "Pre-configure the keyless application from a Clerk application template. Only applies when the strategy resolves to keyless — errors otherwise", + "Pre-configure the accountless application from a Clerk application template. Only applies when the strategy resolves to accountless — 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", + "Replace an existing unclaimed accountless application with a new one, instead of keeping it. Only applies when the strategy resolves to accountless — errors otherwise", ) .option("-y, --yes", "Skip confirmation prompts") .option("--no-skills", "Skip the optional agent skills install prompt") @@ -691,20 +729,20 @@ export function registerInit(program: Program): void { description: "Bootstrap with Bun", }, { - command: "clerk init --starter --framework next --keyless", + command: "clerk init --starter --framework next --accountless", 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", + description: "Log in and link a real application instead of accountless keys", }, { command: "clerk init --template b2b-saas", - description: "Bootstrap a keyless app pre-configured for B2B SaaS", + description: "Bootstrap an accountless 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 --accountless --fresh", + description: "Replace an existing unclaimed accountless 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 472c9f9ba..07cabe23a 100644 --- a/packages/cli-core/src/commands/init/strategy.test.ts +++ b/packages/cli-core/src/commands/init/strategy.test.ts @@ -75,39 +75,39 @@ describe("init strategy", () => { setup(); await expect(init({ template: "b2b-saas", login: true })).rejects.toThrow( - /--template applies to keyless applications/, + /--template applies to accountless applications/, ); expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); }); - test("--keyless with --login throws a usage error before bootstrapping", async () => { + test("--accountless 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/, + await expect(init({ accountless: true, login: true })).rejects.toThrow( + /--accountless 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 () => { + test("--accountless 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/, + await expect(init({ accountless: true, app: "app_abc" })).rejects.toThrow( + /--accountless 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 () => { + test("--accountless on a supported framework uses accountless mode without logging in", async () => { setup(); mockBootstrapTo(KEYLESS_CTX); mockMiddlewareScaffold(); - await init({ keyless: true }); + await init({ accountless: true }); expect(bootstrapMod.promptAndBootstrap).toHaveBeenCalled(); expect(heuristics.printKeylessInfo).toHaveBeenCalled(); @@ -116,19 +116,41 @@ describe("init strategy", () => { expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); }); - test("--keyless takes precedence over an authed user", async () => { - setup({ email: "user@example.com" }); + test("deprecated --keyless behaves as --accountless and warns", async () => { + const { captured } = setup(); mockBootstrapTo(KEYLESS_CTX); mockMiddlewareScaffold(); await init({ keyless: true }); + expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + expect(captured.err).toContain("`--keyless` is deprecated. Use `--accountless` instead."); + }); + + test("deprecated --keyless with --login throws the same usage error as --accountless", async () => { + setup(); + + await expect(init({ keyless: true, login: true })).rejects.toThrow( + /--accountless and --login cannot be combined/, + ); + expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); + expect(loginMod.login).not.toHaveBeenCalled(); + }); + + test("--accountless takes precedence over an authed user", async () => { + setup({ email: "user@example.com" }); + mockBootstrapTo(KEYLESS_CTX); + mockMiddlewareScaffold(); + + await init({ accountless: 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 () => { + test("--accountless on an unsupported framework throws a usage error", async () => { setup(); const nonKeylessCtx: FakeCtx = { ...FAKE_CTX, @@ -144,17 +166,19 @@ describe("init strategy", () => { }; mockBootstrapTo(nonKeylessCtx); - await expect(init({ keyless: true })).rejects.toThrow(/--keyless is not supported for Vue/); + await expect(init({ accountless: true })).rejects.toThrow( + /--accountless 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 () => { + test("--accountless on an existing supported project uses accountless mode", async () => { setup(); mockExistingProject(KEYLESS_CTX); mockMiddlewareScaffold(); - await init({ keyless: true }); + await init({ accountless: true }); expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); expect(keylessMod.createAccountlessApp).toHaveBeenCalled(); @@ -210,12 +234,12 @@ describe("init strategy", () => { expect(linkMod.link).not.toHaveBeenCalled(); }); - test("-y --keyless with keyless framework uses keyless mode", async () => { + test("-y --accountless with a supported framework uses accountless mode", async () => { setup(); mockBootstrapTo(KEYLESS_CTX); mockMiddlewareScaffold(); - await init({ yes: true, keyless: true }); + await init({ yes: true, accountless: true }); expect(heuristics.printKeylessInfo).toHaveBeenCalled(); expect(linkMod.link).not.toHaveBeenCalled(); @@ -262,12 +286,12 @@ describe("init strategy", () => { expect(pullMod.pull).toHaveBeenCalled(); }); - test("agent mode with --keyless uses keyless mode without authentication", async () => { + test("agent mode with --accountless uses accountless mode without authentication", async () => { setup({ isAgent: true, email: null }); mockExistingProject(KEYLESS_CTX); mockMiddlewareScaffold(); - await init({ keyless: true }); + await init({ accountless: true }); expect(heuristics.printKeylessInfo).toHaveBeenCalled(); expect(linkMod.link).not.toHaveBeenCalled(); @@ -521,7 +545,7 @@ describe("init strategy", () => { expect(confirmSpy).toHaveBeenCalledWith( expect.objectContaining({ - message: expect.stringContaining("already has an unclaimed keyless application"), + message: expect.stringContaining("already has an unclaimed accountless application"), default: false, }), ); @@ -621,7 +645,7 @@ describe("init strategy", () => { setup(); await expect(init({ fresh: true, login: true })).rejects.toThrow( - /--fresh applies to keyless applications/, + /--fresh applies to accountless applications/, ); expect(bootstrapMod.promptAndBootstrap).not.toHaveBeenCalled(); }); @@ -648,7 +672,7 @@ describe("init strategy", () => { mockExistingProject(KEYLESS_CTX); await expect(init({ template: "b2b-saas", yes: true })).rejects.toThrow( - /--template only applies to keyless applications/, + /--template only applies to accountless applications/, ); expect(keylessMod.createAccountlessApp).not.toHaveBeenCalled(); }); @@ -658,7 +682,7 @@ describe("init strategy", () => { mockExistingProject(KEYLESS_CTX); await expect(init({ fresh: true, yes: true })).rejects.toThrow( - /--fresh only applies to keyless applications/, + /--fresh only applies to accountless applications/, ); }); @@ -667,7 +691,7 @@ describe("init strategy", () => { 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/, + /--template only applies to accountless applications/, ); }); @@ -687,7 +711,33 @@ describe("init strategy", () => { }; spyOn(context, "gatherContext").mockResolvedValue(nonKeylessCtx); - await expect(init({ template: "b2b-saas" })).rejects.toThrow(/does not support keyless mode/); + await expect(init({ template: "b2b-saas" })).rejects.toThrow( + /does not support accountless setup/, + ); + }); + + test("--template on a non-keyless framework in human mode names the missing keyless support", async () => { + setup({ email: null }); + 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); + + // Human mode resolves an unsupported framework to the authenticated + // flow, so the guard must not suggest --accountless here. + await expect(init({ template: "b2b-saas" })).rejects.toThrow( + /does not support accountless setup/, + ); + expect(loginMod.login).not.toHaveBeenCalled(); }); test("--template on a keyless-resolved run is still forwarded normally", async () => { diff --git a/packages/cli-core/src/commands/open/README.md b/packages/cli-core/src/commands/open/README.md index 1d769f317..50258c787 100644 --- a/packages/cli-core/src/commands/open/README.md +++ b/packages/cli-core/src/commands/open/README.md @@ -1,6 +1,6 @@ # 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. +Opens the linked Clerk application's dashboard in your browser. When the current directory isn't linked but holds an unclaimed accountless application instead, opens that application's one-time **claim link** rather than failing. ## Usage @@ -23,18 +23,18 @@ clerk open --print # Print the URL instead of opening a browser 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. +3. **Not linked** — falls through to the accountless 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 +### Unclaimed accountless 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): +`clerk link` cannot help an accountless 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}`. +2. `.clerk/keyless.json` — the legacy-named breadcrumb `clerk init --accountless` 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. +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 the legacy-named `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: @@ -46,7 +46,7 @@ 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). +- 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 --accountless` 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 | @@ -55,8 +55,8 @@ When **no** claim link can be found at all, the error differs depending on what ## Output modes -- **`--print`** — the bare URL on stdout, nothing else. Works identically for the dashboard deep-link and the keyless claim link. +- **`--print`** — the bare URL on stdout, nothing else. Works identically for the dashboard deep-link and the accountless 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 }`. + - Accountless: `{ url, accountless: true, keyless: true, claimSource, instanceId, environmentType, subpath: null, opened: false }` — `keyless` is a deprecated alias of `accountless`, kept for agents that still parse the legacy key (same treatment as `whoami`'s JSON output). - **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 be7a71711..8d79dcdc6 100644 --- a/packages/cli-core/src/commands/open/index.test.ts +++ b/packages/cli-core/src/commands/open/index.test.ts @@ -277,7 +277,7 @@ describe("openDashboard: unclaimed keyless application", () => { expect(mockOpenBrowser).not.toHaveBeenCalled(); }); - test("agent mode: emits structured JSON with keyless: true, no browser", async () => { + test("agent mode: emits structured JSON with accountless: true (and the deprecated keyless alias), no browser", async () => { setMode("agent"); mockFindKeylessClaimUrl.mockResolvedValue(CLAIM_DESTINATION); mockResolveKeylessTarget.mockResolvedValue({ secretKey: "sk_test_x", source: ".env" }); @@ -287,6 +287,7 @@ describe("openDashboard: unclaimed keyless application", () => { const payload = JSON.parse(captured.out); expect(payload).toEqual({ url: CLAIM_DESTINATION.url, + accountless: true, keyless: true, claimSource: CLAIM_DESTINATION.source, instanceId: "ins_keyless123", @@ -329,7 +330,7 @@ describe("openDashboard: unclaimed keyless application", () => { mockFindKeylessClaimUrl.mockResolvedValue(undefined); mockResolveKeylessTarget.mockResolvedValue({ secretKey: "sk_test_x", source: ".env.local" }); - await expect(openDashboard(undefined)).rejects.toThrow(/clerk init --keyless/); + await expect(openDashboard(undefined)).rejects.toThrow(/clerk init --accountless/); 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 134a206c6..c1ae8a91b 100644 --- a/packages/cli-core/src/commands/open/index.ts +++ b/packages/cli-core/src/commands/open/index.ts @@ -106,7 +106,7 @@ export async function openDashboard( } /** - * The keyless counterpart to `openDashboard` above. An unclaimed keyless + * The accountless counterpart to `openDashboard` above. An unclaimed accountless * 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 @@ -130,13 +130,13 @@ async function openKeylessDashboard( 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.", + "Otherwise, run `clerk init --accountless` 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. " + + "No Clerk project linked to this directory, and no accountless application was found either. " + "Run `clerk link` if you already have an application, or `clerk init` to create one.", { code: ERROR_CODE.NOT_LINKED }, ); @@ -172,6 +172,9 @@ async function openKeylessDashboard( log.data( JSON.stringify({ url, + accountless: true, + // Deprecated alias of `accountless`, kept for agents that still parse + // the legacy key. keyless: true, claimSource: source, instanceId: instance?.instanceId ?? null, diff --git a/packages/cli-core/src/commands/open/keyless-claim.ts b/packages/cli-core/src/commands/open/keyless-claim.ts index a9b6a19d2..17c9e5be0 100644 --- a/packages/cli-core/src/commands/open/keyless-claim.ts +++ b/packages/cli-core/src/commands/open/keyless-claim.ts @@ -68,7 +68,8 @@ async function readSdkClaimUrl(cwd: string): Promise { * 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` + * - `clerk init --accountless` writes only the claim TOKEN to the legacy-named + * `.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) @@ -110,7 +111,9 @@ export async function describeKeylessInstance(secretKey: string): Promise` (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` +- this project's own accountless 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 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 third step is what makes `clerk users` work with no login and no Platform API auth on an **unclaimed accountless 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. @@ -124,11 +124,11 @@ 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 +#### On an unclaimed accountless 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. +This is the one command in the family that an unclaimed accountless 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. +Rather than report the generic "no Clerk project linked", `users open` detects the accountless 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 accountless one in this directory. ## API Endpoints diff --git a/packages/cli-core/src/commands/users/open.test.ts b/packages/cli-core/src/commands/users/open.test.ts index f1af97082..593d035f5 100644 --- a/packages/cli-core/src/commands/users/open.test.ts +++ b/packages/cli-core/src/commands/users/open.test.ts @@ -100,7 +100,7 @@ describe("users open", () => { 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, + /unclaimed accountless application \(secret key from \.env\.local\).*no Dashboard page/s, ); }); diff --git a/packages/cli-core/src/commands/whoami/README.md b/packages/cli-core/src/commands/whoami/README.md index 7e7a29932..b3a08e7e5 100644 --- a/packages/cli-core/src/commands/whoami/README.md +++ b/packages/cli-core/src/commands/whoami/README.md @@ -22,33 +22,36 @@ 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, falls back to the keyless path below; when that finds nothing either, throws an `AuthError` ("Not logged in"). +- If no token exists, falls back to the accountless path below; when that finds nothing either, throws an `AuthError` ("Not logged in"). -### Keyless applications +### Accountless 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. 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)". +An unclaimed accountless 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 { "email": null, - "keyless": { + "accountless": { "instanceId": "ins_...", "environmentType": "development", "publishableKey": "pk_test_...", "publishableKeyMismatch": false, "keySource": ".clerk/.tmp/keyless.json" }, + "keyless": { "…": "deprecated alias — same object as `accountless`" }, "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)). +`keyless` is a deprecated alias of `accountless`, carrying the identical object, kept for agents that still parse the legacy key. + +`publishableKey` and the secret key are found independently (see `findLocalSecretKey`/`findLocalPublishableKey` in `lib/keyless-target.ts`) and can each belong to a _different_ accountless 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. | +| `GET` | `/v1/instance` | Reads the accountless 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 61fe89c88..4b6e09b35 100644 --- a/packages/cli-core/src/commands/whoami/index.test.ts +++ b/packages/cli-core/src/commands/whoami/index.test.ts @@ -319,16 +319,18 @@ describe("whoami", () => { await runWhoami({ json: true }); + const instance = { + instanceId: "ins_keyless_1", + environmentType: "development", + publishableKey: "pk_test_keyless", + publishableKeyMismatch: false, + keySource: ".env.local", + }; 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", - }, + accountless: instance, + keyless: instance, }); }); @@ -420,7 +422,7 @@ describe("whoami", () => { await runWhoami(); expect(captured.out.trim()).toBe("ins_keyless_1"); - expect(captured.err).toContain("unclaimed keyless application"); + expect(captured.err).toContain("unclaimed accountless 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 832105499..0b6da93f8 100644 --- a/packages/cli-core/src/commands/whoami/index.ts +++ b/packages/cli-core/src/commands/whoami/index.ts @@ -124,7 +124,7 @@ async function describeKeyless(keyless: KeylessTarget): Promise { const instance = await withSpinner("Fetching instance info...", async () => { const response = await withApiContext( bapiRequest({ method: "GET", path: "/v1/instance", secretKey: keyless.secretKey }), - `Failed to read the keyless secret key from \`${keyless.source}\``, + `Failed to read the accountless secret key from \`${keyless.source}\``, ); return response.body as { id?: string; environment_type?: string }; }); @@ -172,7 +172,9 @@ async function checkKeyPairMismatch( function toJson(identity: Identity): Record { if (identity.kind === "keyless") { const { kind: _kind, ...keyless } = identity; - return { email: null, keyless, linked: null }; + // `keyless` is a deprecated alias of `accountless`, kept for agents that + // still parse the legacy key. + return { email: null, accountless: keyless, keyless, linked: null }; } const resolved = identity.profile; @@ -198,7 +200,7 @@ 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}\`)`, + `Not logged in — running on an unclaimed accountless application (key from \`${identity.keySource}\`)`, ); // Not NEXT_STEPS.WHOAMI: `clerk link` needs an application in an account, // which an unclaimed app by definition isn't — the same dead end diff --git a/packages/cli-core/src/lib/bapi-command.test.ts b/packages/cli-core/src/lib/bapi-command.test.ts index a984b128a..6cc0e2800 100644 --- a/packages/cli-core/src/lib/bapi-command.test.ts +++ b/packages/cli-core/src/lib/bapi-command.test.ts @@ -329,7 +329,7 @@ describe("bapi-command", () => { resolveKeylessTargetSpy.mockResolvedValue({ secretKey: "sk_test_123", source: ".env.local" }); await expect(describeBapiTarget({})).resolves.toBe( - "this keyless application (secret key from .env.local)", + "this accountless application (secret key from .env.local)", ); 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 26f6f24a4..0607325d6 100644 --- a/packages/cli-core/src/lib/bapi-command.ts +++ b/packages/cli-core/src/lib/bapi-command.ts @@ -33,7 +33,7 @@ export async function describeBapiTarget( cwd: options.cwd, }); if (keyless) { - return `this keyless application (secret key from ${keyless.source})`; + return `this accountless application (secret key from ${keyless.source})`; } try { diff --git a/packages/cli-core/src/lib/copy.ts b/packages/cli-core/src/lib/copy.ts index 2e287b74f..e8c12933e 100644 --- a/packages/cli-core/src/lib/copy.ts +++ b/packages/cli-core/src/lib/copy.ts @@ -1,5 +1,5 @@ /** - * User-facing copy for the keyless surface, gathered in one module. + * User-facing copy for the accountless 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 @@ -17,11 +17,11 @@ 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` + + `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed accountless 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(", ")}.`, + `Unsupported config ${unknown.length === 1 ? "key" : "keys"} for an unclaimed accountless application: ${unknown.join(", ")}.`, supportedPayloadKeysLine: (supported: readonly string[]): string => `Supported top-level keys: ${supported.join(", ")}.`, @@ -35,7 +35,7 @@ export const keylessCopy = { 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(", ")}.`, + `Unsupported ${unknown.length === 1 ? "field" : "fields"} on \`instance\` for an unclaimed accountless application: ${unknown.join(", ")}.`, supportedInstanceFieldsLine: (fields: readonly string[]): string => `Supported fields: ${fields.join(", ")}.`, @@ -46,19 +46,19 @@ export const keylessCopy = { // --- "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" + + "Billing can only be configured on a claimed application — Clerk's Backend API has no billing settings, so an unclaimed accountless 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" + + "Config schema is only available for a claimed application — the schema describes the account-level config document, which an unclaimed accountless 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" + + "Replacing the entire configuration is only available for a claimed application — an unclaimed accountless 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` + + `This directory holds an unclaimed accountless 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.`, }; diff --git a/packages/cli-core/src/lib/keyless-target.ts b/packages/cli-core/src/lib/keyless-target.ts index c1f7f5b08..99a63edbb 100644 --- a/packages/cli-core/src/lib/keyless-target.ts +++ b/packages/cli-core/src/lib/keyless-target.ts @@ -123,7 +123,7 @@ export async function findLocalSecretKey(cwd: string): Promise { @@ -168,7 +168,7 @@ export async function peekKeylessBreadcrumb(cwd: string): Promise { try { await unlink(breadcrumbPath(cwd)); - log.debug("Cleared keyless breadcrumb"); + log.debug("Cleared accountless breadcrumb"); } catch { // idempotent }