Skip to content

feat: auto-claim keyless applications on login#157

Open
rafa-thayto wants to merge 9 commits intomainfrom
rafa-thayto/auto-claim
Open

feat: auto-claim keyless applications on login#157
rafa-thayto wants to merge 9 commits intomainfrom
rafa-thayto/auto-claim

Conversation

@rafa-thayto
Copy link
Copy Markdown
Contributor

@rafa-thayto rafa-thayto commented Apr 15, 2026

Summary

  • Add keyless application lifecycle (keyless.ts): create accountless apps via BAPI, write framework-specific keys to .env.local, manage .clerk/keyless.json claim token breadcrumb
  • Add autoclaim orchestrator (autoclaim.ts): after clerk auth login, detect keyless breadcrumb and automatically claim the application via PLAPI — never throws, returns discriminated union so login is never interrupted
  • Auto-pull environment variables after successful claim — no manual clerk env pull step needed
  • Extract shared linkApp() from autolink.ts for reuse by both autolink (key detection) and autoclaim (keyless claim)
  • Add claimApplication() PLAPI endpoint (POST /v1/platform/accountless_applications/claim)
  • Integrate keyless setup into clerk init bootstrap when user skips auth
  • Integrate autoclaim into clerk auth login with contextual warnings and next-steps per failure mode
  • Truncate app name to 50 chars to match backend validation limit

How it works

  1. clerk init (skip auth) → creates accountless app, writes keys to .env.local, stores claim token in .clerk/keyless.json
  2. Developer builds their app using temporary dev keys
  3. clerk auth login → after OAuth, reads breadcrumb, calls claim endpoint, links app to account, auto-pulls env vars, clears breadcrumb
  4. Failure handling: transient errors (5xx) preserve breadcrumb for retry on next login; terminal errors (404/403) clear it; env pull failures warn but don't block login

Cleanup pass (commit 6cda4f5)

Applied KISS/DRY simplification across the new surface and touched neighboring code where it shared the same patterns. Net: -121 lines (107 insertions, 228 deletions).

  • plapi.ts: extracted shared plapiRequest<T> helper; 6 near-identical fetch-with-auth blocks (20 lines each) collapsed to 2–3 line call sites. Helper normalizes URL construction, query-param serialization, auth headers, PlapiError throw, and JSON parsing.
  • errors.ts: promoted errorMessage(unknown): string out of commands/doctor/checks.ts so autoclaim.ts and init/index.ts can reuse it instead of re-implementing error instanceof Error ? error.message : String(error) inline.
  • keyless.ts: replaced two TOCTOU Bun.file(...).exists() + read patterns with atomic .text().catch(() => "") / .json().catch(...); parallelized independent detectPublishableKeyName + detectSecretKeyName with Promise.all; factored .env.local / .clerk / keyless.json into named constants.
  • autoclaim.ts: table-driven classifyClaimError — replaced back-to-back error instanceof PlapiError && error.status === N branches with a TERMINAL_BY_STATUS: Record<number, Terminal["status"]> lookup.
  • Comments: removed file-level docstring blocks, section-header banners, and WHAT/narration comments from new files per house style (kept non-obvious WHY comments).
  • Tests: as neveras Profile, dropped the spies[] array in favor of explicit per-spy mockRestore(), deleted redundant setup commentary.

Test plan

  • 9 unit tests for autoclaim (all status paths, breadcrumb preservation, env pull success/failure)
  • 15 unit tests for keyless (BAPI calls, env writing, breadcrumb I/O, token parsing)
  • Login tests pass with autoclaim mocked
  • Full test suite passes (73 suites)
  • Lint, format, and typecheck clean
  • CI green after cleanup pass
  • Requires backend PLAPI claim endpoint to be deployed first (CLI gracefully no-ops if endpoint is unavailable)
  • E2E: clerk init -y → skip auth → verify .env.local and .clerk/keyless.json created → clerk auth login → verify app claimed, linked, and env vars refreshed

@rafa-thayto rafa-thayto marked this pull request as ready for review April 17, 2026 17:11
@rafa-thayto rafa-thayto force-pushed the rafa-thayto/auto-claim branch from a56b83d to d338623 Compare April 17, 2026 17:11
@changeset-bot
Copy link
Copy Markdown

changeset-bot bot commented Apr 17, 2026

⚠️ No Changeset found

Latest commit: 6cda4f5

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

📝 Walkthrough

Walkthrough

This pull request introduces automatic claiming of keyless applications during the login flow. After a keyless app is created during initialization (with temporary keys persisted via a breadcrumb file), the new attemptAutoclaim function is invoked when users authenticate. It reads the persisted claim token, calls a new PLAPI endpoint to claim the application, links it to the project directory via a refactored linkApp function, and optionally pulls environment configuration. The implementation includes breadcrumb management (write, read, clear), environment key writing, claim token parsing, and updated user messaging reflecting the simplified flow.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 21.74% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The PR title 'feat: auto-claim keyless applications on login' directly and concisely describes the main change: adding automatic claiming of keyless applications during the login flow.
Description check ✅ Passed The pull request description comprehensively details the keyless application lifecycle, autoclaim orchestrator, and all integration points with clear examples of the feature flow.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@rafa-thayto rafa-thayto force-pushed the rafa-thayto/auto-claim branch from d338623 to 76b9642 Compare April 17, 2026 17:12
Add machine-readable error code for autoclaim failures, available
for agent mode and CI error classification.
Add PLAPI client function for POST /v1/platform/accountless_applications/claim.
Sends claim token and app name, returns the claimed Application.
Extract profile-building logic from autolink() into a reusable linkApp()
function. This allows both autolink (key detection) and autoclaim
(keyless claim) to share the same profile persistence code.
Add keyless.ts for managing accountless Clerk applications:
- createAccountlessApp(): calls BAPI to create app with no auth
- writeKeysToEnvFile(): writes framework-specific keys to .env.local
- parseClaimToken(): extracts token from claim URL
- Breadcrumb I/O: read/write/clear .clerk/keyless.json

Includes 15 unit tests covering all functions and edge cases.
Add autoclaim.ts that detects keyless projects and claims them
automatically after clerk auth login. Never throws — returns a
discriminated union (claimed|not_found|already_claimed|failed|not_keyless)
so the login flow is never interrupted.

Key behaviors:
- Truncates app name to 50 chars (backend limit)
- Transient errors (5xx) preserve breadcrumb for retry
- Terminal errors (404/403) clear breadcrumb

Includes 8 unit tests covering all status paths.
When bootstrap mode skips authentication, create an accountless app
via BAPI, write keys to .env.local, and store the claim token in
.clerk/keyless.json for autoclaim on next login.

Also simplifies the keyless info message to promote the autoclaim
flow instead of requiring manual clerk link + env pull.
Wire autoclaim into the login flow with improved UX:
- Specific warning messages per failure cause (expired token,
  missing org, transient error) instead of generic fallback
- Contextual next-steps based on claim result: manual link for
  terminal failures, retry guidance for transient errors
- Mock autoclaim in login tests to isolate from transitive deps
After a successful autoclaim, automatically run `env pull` to refresh
.env.local with the claimed app's keys — no manual step needed.
@rafa-thayto rafa-thayto force-pushed the rafa-thayto/auto-claim branch from 76b9642 to 55ea73e Compare April 17, 2026 22:17
- Extract shared plapiRequest<T> helper; collapse 6 PLAPI functions to
  2-3 lines each (6 near-duplicate fetch blocks → one)
- Promote errorMessage() from doctor/checks.ts to lib/errors.ts so it
  can be shared with autoclaim.ts and init/index.ts
- Replace two TOCTOU file.exists()+read patterns in keyless.ts with
  atomic .text()/.json() catch-ENOENT variants
- Parallelize detectPublishableKeyName + detectSecretKeyName in
  writeKeysToEnvFile (independent I/O)
- Table-drive classifyClaimError (404/403 status → result mapping)
- Drop file-level docstrings, section-header banners, and WHAT/
  narration comments across new files per house style
Copy link
Copy Markdown
Contributor

@wyattjoh wyattjoh left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review — PR #157

Reviewed with Opus + Codex second-opinion validation.

Blocker

B1. .clerk/keyless.json stores a live claim token with no .gitignore protection (codex confirmed)
packages/cli-core/src/lib/keyless.ts:88-106 writes claimToken under .clerk/ in the user's project. Nothing in this PR (or clerk init today) appends .clerk/ to the project's .gitignore. A developer can git add . and commit the token to a public repo, at which point any third party can claim the app before the original developer logs in, hijacking an app that will later hold production workloads.

Suggestion: in setupKeylessApp or writeKeylessBreadcrumb, append .clerk/ to .gitignore if not already present (mirroring what create-next-app and similar tools do). Add a test that verifies the .gitignore update.

B2. Keyless init writes .env.local but post-claim pull writes the framework default, producing two env files with drifting keys
This is a cross-file interaction with existing code rather than a change in this PR alone, but the PR is where the regression becomes user-visible:

  • packages/cli-core/src/lib/keyless.ts:57-76 hardcodes const targetFile = join(cwd, ".env.local").
  • packages/cli-core/src/lib/framework.ts (unchanged) declares envFile: ".env" for Next.js, Astro, and Nuxt.
  • packages/cli-core/src/commands/env/pull.ts:30-46 (resolveTargetFile) prefers the framework-detected file.

Net effect on a Next.js project: .env.local written at init, .env written by pull after claim, same keys, different values.

Suggestion: use detectEnvFile(cwd) in writeKeysToEnvFile so there is one canonical target. Also update the JSDoc that references .env.local.

B3. Missing changeset
git diff main..HEAD -- .changeset/ is empty. This is a feat: PR touching @clerk/cli-core runtime. The Enforce Changeset workflow will block merge.

Major

M1. App name sent to backend uses basename(cwd).slice(0, 50) with no sanitization (codex confirmed)
packages/cli-core/src/lib/autoclaim.ts:32:

  • Leaks potentially-sensitive directory names to the Clerk platform API.
  • .slice(50) truncates on UTF-16 code units; an emoji or non-BMP char at the boundary produces an orphaned surrogate (400 on strict UTF-8 validators).
  • Backend-invalid chars are not sanitized.

Suggestion: derive the name from package.json#name or the git repo name with basename(cwd) as a fallback, and sanitize + byte-length-truncate before sending.

M2. attemptAutoclaim(cwd) threads cwd but tryPullEnv ignores it
autoclaim.ts:37-50 accepts a cwd argument for the breadcrumb but calls pull({}), which uses process.cwd() internally (pull.ts:53). Today's single call site passes process.cwd() so it is benign, but this is fragile. Either thread cwd through pull options or assert at entry.

M3. createAccountlessApp has no timeout or abort; clerk init hangs on captive portal / slow BAPI (codex confirmed)
packages/cli-core/src/lib/keyless.ts:35-52 and packages/cli-core/src/commands/init/index.ts:272-287: fetch(...) has no AbortController. On slow/broken networks, withSpinner hangs indefinitely. log.debug is only visible with --verbose, so users see nothing.

M4. setupKeylessApp swallows all errors while success copy still renders (codex confirmed)
commands/init/index.ts:270-287 wraps the body in try { ... } catch { log.debug(...) }. Even on filesystem errors, parseClaimToken throws, or the fetch fails, printKeylessInfo still tells the user "Your app is ready with development keys in .env.local". Distinguish expected network failures from unexpected bugs and suppress the success message on failure.

M5. classifyClaimError semantic mismatch and aggressive breadcrumb-clear
autoclaim.ts:62-74 maps 403 to already_claimed, but the message at login.ts:128-132 says "your account does not have an active organization". Name and message disagree; rename the status (e.g. no_organization). Also, 404 unconditionally clears the breadcrumb; a transient DNS blip that returns 404 would destroy the retry signal.

Minor

  • readKeylessBreadcrumb (keyless.ts:108-119) has no schema validation; a malformed-but-JSON file yields {claimToken: undefined} and autoclaim.ts:33 will POST {token: undefined}.
  • parseClaimToken uses https://placeholder.com as the synthetic URL base (keyless.ts:81-87); prefer https://example.invalid (RFC 6761).
  • Em-dashes added in login.ts:130-131,163 and autoclaim.ts:7 (global style rule).
  • handleAutoclaim prints log.success then log.warn then outro (login.ts:117-124,135-148); noisy ordering.
  • No retry for 5xx / 429 despite the design claim that "transient failures preserve breadcrumb for retry on next login" (autoclaim.ts:28-36); a short in-process retry with backoff would save users a manual re-login.

Nits

  • CLAIM_WARNINGS could be an explicit object-literal map of the three warning statuses.
  • log.info("Environment variables written to .env.local") will be wrong once B2 is fixed.
  • Test expect(parsed.name).toBeTruthy() (autoclaim.test.ts:164) is too loose; assert the exact truncation.
  • CI refactor (composite action, workflow_call) is bundled into a feat: PR; splitting would simplify bisect.

Positives

AutoclaimResult discriminated union is cleanly typed and enables exhaustive handling in loginNextSteps. attemptAutoclaim never throws (the type system enforces it). linkApp extraction in autolink.ts is a good refactor. Test coverage for the orchestrator is thorough: all status paths, breadcrumb clear/preserve semantics, env-pull success/failure, linkApp invocation, and request body shape.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants