Skip to content

feat: auto-claim keyless applications on login#157

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

feat: auto-claim keyless applications on login#157
rafa-thayto wants to merge 10 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

🦋 Changeset detected

Latest commit: a688ed0

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
clerk Minor

Not sure what this means? Click here to learn what changesets are.

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

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 17, 2026

Warning

Rate limit exceeded

@rafa-thayto has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 54 minutes and 23 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 54 minutes and 23 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0a66faee-eae0-4610-8c04-858a2eb7449b

📥 Commits

Reviewing files that changed from the base of the PR and between 55ea73e and a688ed0.

📒 Files selected for processing (13)
  • .changeset/auto-claim.md
  • packages/cli-core/src/commands/auth/login.ts
  • packages/cli-core/src/commands/doctor/checks.ts
  • packages/cli-core/src/commands/doctor/index.ts
  • packages/cli-core/src/commands/env/pull.ts
  • packages/cli-core/src/commands/init/index.ts
  • packages/cli-core/src/lib/autoclaim.test.ts
  • packages/cli-core/src/lib/autoclaim.ts
  • packages/cli-core/src/lib/autolink.ts
  • packages/cli-core/src/lib/errors.ts
  • packages/cli-core/src/lib/keyless.test.ts
  • packages/cli-core/src/lib/keyless.ts
  • packages/cli-core/src/lib/plapi.ts
📝 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.

B1: add .clerk/ to .gitignore in writeKeylessBreadcrumb to prevent
    claim token from being committed to public repos

B2: use detectEnvFile(cwd) in writeKeysToEnvFile so init and claim
    write to the same env file (e.g. .env for Next.js, not .env.local)

B3: add changeset (minor) for the autoclaim feature

M1: derive app name from package.json#name with basename fallback;
    byte-length-truncate to 50 to avoid surrogate issues

M2: thread cwd through tryPullEnv -> pull() instead of relying on
    process.cwd() internally

M3: add 15s AbortController timeout to createAccountlessApp to
    prevent hanging on captive portal / slow BAPI

M4: move printKeylessInfo() inside try block so success copy is
    suppressed on failure

M5: rename already_claimed -> no_organization to match the actual
    403 semantic (no active organization); fix em-dashes in messages

nit: use https://example.invalid instead of https://placeholder.com
nit: add schema validation to readKeylessBreadcrumb
nit: tighten autoclaim test name assertion (exact value, not truthy)
nit: add gitignore idempotency test + wrong-shape breadcrumb test

docs(changeset): Automatically claim and link keyless applications on clerk auth login
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

Substantial new surface (keyless.ts, autoclaim.ts) plus a clean plapi.ts refactor. Core design — discriminated AutoclaimResult, never-throws contract, breadcrumb-preserve-on-transient — is sound. A handful of issues worth tightening before merge: one user-facing inaccuracy (.env.local message), one misleading API parameter (pull({ cwd })), and a few error-handling edges (retry loops, corrupted breadcrumb recovery, silent keyless creation failures).

" clerk auth login",
" clerk link",
" clerk env pull",
"\n Your app is ready with development keys in .env.local.",
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] printKeylessInfo hardcodes .env.local, but Next.js/Astro/Nuxt write to .env

The new message says Your app is ready with development keys in .env.local. But writeKeysToEnvFile (in lib/keyless.ts) resolves the target via detectEnvFile(cwd), which returns .env for Next.js, Astro, and Nuxt per FRAMEWORK_MAP. A Next.js user following this output will look in the wrong file.

Consider accepting the resolved envFile as a parameter (the init flow already has it in ctx.envFile) and interpolating it into the message. At minimum, replace the hardcoded .env.local with a generic phrase.

Evidence

heuristics.ts:
"\n Your app is ready with development keys in .env.local.",

framework.ts FRAMEWORK_MAP:
{ dep: "next", envFile: ".env", supportsKeyless: true },
{ dep: "astro", envFile: ".env", supportsKeyless: true },
{ dep: "nuxt", envFile: ".env", supportsKeyless: true },

export async function pull(options: EnvPullOptions): Promise<void> {
const ctx = await resolveAppContext(options);
const cwd = process.cwd();
const cwd = options.cwd ?? process.cwd();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[HIGH] pull({ cwd }) threads cwd partially — resolveAppContext still uses process.cwd()

The new cwd option is honored for detectEnvFile, resolveTargetFile, and detectPublishableKeyName, but resolveAppContext(options) at line 52 still calls resolveProfile(process.cwd()) internally (see lib/config.ts:287). If a future caller passes a cwd that differs from process.cwd(), profile lookup silently uses the wrong directory.

Today the only consumer (autoclaim.attemptAutoclaim) passes process.cwd(), so there is no live bug — but the API is misleading. Either plumb cwd through resolveAppContextresolveProfile, or drop the cwd option entirely and document that pull uses process.cwd(). The lean fix is the latter since the only caller happens to be same-cwd.

Evidence

pull.ts:
export async function pull(options: EnvPullOptions): Promise {
const ctx = await resolveAppContext(options); // <-- options.cwd ignored here
const cwd = options.cwd ?? process.cwd();
...

config.ts (resolveAppContext):
const resolved = await resolveProfile(process.cwd()); // hardcoded


const APP_NAME_MAX_BYTES = 50;

const TERMINAL_BY_STATUS: Record<number, Terminal["status"]> = {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] Non-404/403 PlapiErrors preserve breadcrumb → infinite retry on terminal 4xx

classifyClaimError only treats 404 and 403 as terminal. A 400 (invalid token), 409 (already claimed elsewhere), or 422 (validation) all fall through to failed, which preserves the breadcrumb and retries on every subsequent clerk auth login. The user sees the same "Auto-claim failed due to a temporary error" warning repeatedly with no path to recovery short of manually deleting .clerk/keyless.json.

Broaden TERMINAL_BY_STATUS to cover all 4xx except 408/429, or explicitly list 400, 409, 422 alongside 403/404. 5xx and network errors should remain retryable.

Evidence

const TERMINAL_BY_STATUS: Record<number, Terminal["status"]> = {
404: "not_found",
403: "no_organization",
};

function classifyClaimError(error: unknown): Terminal | Failed {
if (error instanceof PlapiError && error.status in TERMINAL_BY_STATUS) { ... }
// everything else -> Failed, breadcrumb preserved
return { status: "failed", error: err };
}

export async function readKeylessBreadcrumb(cwd: string): Promise<KeylessBreadcrumb | undefined> {
try {
const data: unknown = await Bun.file(breadcrumbPath(cwd)).json();
return isKeylessBreadcrumb(data) ? data : undefined;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] Corrupted keyless.json is silently ignored forever

readKeylessBreadcrumb swallows parse errors and shape-validation failures, returning undefined. A corrupted keyless.json (truncated write, user edit, wrong shape) will cause every subsequent clerk auth login to report not_keyless silently — the user never learns why auto-claim is skipped.

When isKeylessBreadcrumb(data) returns false, call clearKeylessBreadcrumb(cwd) (and log.debug the reason). That way the file is recoverable by re-running clerk init --keyless, and the state doesn't persist indefinitely.

Evidence

export async function readKeylessBreadcrumb(cwd: string): Promise<KeylessBreadcrumb | undefined> {
try {
const data: unknown = await Bun.file(breadcrumbPath(cwd)).json();
return isKeylessBreadcrumb(data) ? data : undefined; // malformed file stays on disk
} catch {
return undefined;
}
}

} catch (error) {
log.debug(`Could not create accountless app: ${errorMessage(error)}`);
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] setupKeylessApp swallows creation failures at debug log level

When createAccountlessApp fails (BAPI down, network error, timeout), the catch block only emits log.debug, which is suppressed without --verbose. The user sees zero output for the keyless step: no warning, no next-steps, nothing. printKeylessInfo() is never reached. The outro("Done") still prints. The user then runs bun dev and wonders why auth doesn't work.

Escalate the failure case to log.warn and print a brief recovery hint (clerk auth login then clerk link). log.debug is fine for the underlying error detail, but the user needs to know the keyless step failed.

Evidence

async function setupKeylessApp(cwd: string, frameworkDep: string): Promise {
try {
const app = await withSpinner("Creating development application...", () => ...);
await writeKeysToEnvFile(...);
await writeKeylessBreadcrumb(...);
printKeylessInfo();
} catch (error) {
log.debug(Could not create accountless app: ${errorMessage(error)}); // silent to the user
}
}


if (!response.ok) {
const text = await response.text();
throw new Error(`Failed to create accountless application (${response.status}): ${text}`);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[MEDIUM] createAccountlessApp bypasses the project's error-handling protocol

The BAPI call throws a plain new Error(...) with the raw status/body. Per .claude/rules/errors.md, API errors should use a typed error class that the global handler recognizes (BapiError/PlapiError) and be wrapped with withApiContext for user-facing context. As written, a non-OK response surfaces as Failed to create accountless application (500): <raw body> which is both unstyled and leaks response body directly.

Prefer BapiError if one exists (the rules doc references src/commands/api/bapi.ts), or wrap with an error class the global handler can format.

Evidence

if (!response.ok) {
const text = await response.text();
throw new Error(Failed to create accountless application (${response.status}): ${text});
}

DOCTOR_FAILED: "doctor_failed",
/** Autoclaim of a keyless application failed. */
AUTOCLAIM_FAILED: "autoclaim_failed",
} as const;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] AUTOCLAIM_FAILED error code added but never thrown

ERROR_CODE.AUTOCLAIM_FAILED is added to the enum but nothing in autoclaim.ts throws a CliError with it. The autoclaim design is explicitly "never throws" and uses a discriminated-union return type, so there is no consumer for this code. Dead code — either remove it or wire it into the failed branch's Error.

Evidence

errors.ts:
/** Autoclaim of a keyless application failed. */
AUTOCLAIM_FAILED: "autoclaim_failed",

autoclaim.ts never references ERROR_CODE.AUTOCLAIM_FAILED.

}
return basename(cwd);
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] truncateToByteLength uses byte-slice + lenient decode; consider char-based truncation

The current impl slices UTF-8 bytes, decodes with fatal: false, and strips a trailing U+FFFD replacement char. This works but is brittle: it creates an extra buffer copy and the intent is non-obvious. Per the PR body the backend limit is "50 chars". If that is codepoints (likely) rather than UTF-8 bytes, Array.from(str).slice(0, 50).join('') is both simpler and matches the "chars" framing.

Worth confirming with the backend team what the 50 actually counts — bytes, codepoints, or JS-string-length — and matching that exactly. Emoji/CJK app names are the edge case.

Evidence

const APP_NAME_MAX_BYTES = 50;

function truncateToByteLength(str: string, maxBytes: number): string {
const encoded = new TextEncoder().encode(str);
if (encoded.length <= maxBytes) return str;
return new TextDecoder("utf-8", { fatal: false })
.decode(encoded.slice(0, maxBytes))
.replace(/\uFFFD$/, "");
}

}

export function parseClaimToken(claimUrl: string): string {
const base = claimUrl.startsWith("http") ? undefined : "https://example.invalid";
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] example.invalid base URL sentinel deserves a one-line comment

parseClaimToken uses https://example.invalid as a base URL so WHATWG URL can parse relative paths like /apps/claim?token=abc. The pattern is correct but the intent is non-obvious to readers who haven't hit the WHATWG URL relative-parsing quirk. A one-liner (// Need a dummy base because WHATWG URL rejects relative URLs without one) would save the next reader a confused minute.

Evidence

export function parseClaimToken(claimUrl: string): string {
const base = claimUrl.startsWith("http") ? undefined : "https://example.invalid";
const token = new URL(claimUrl, base).searchParams.get("token");
...
}


return response.json() as Promise<Application>;
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[LOW] include_secret_keys=true now serializes as the string "true"

fetchApplication previously called url.searchParams.set("include_secret_keys", "true"). The refactored version passes search: { include_secret_keys: true }, and applySearch handles value === true by writing "true". Behavior is identical to the old code (both produce ?include_secret_keys=true), but worth a targeted spot-check against a live PLAPI instance to confirm the backend parses the query string identically after the refactor — especially for the destructive flag in sendInstanceConfig, which is only set when truthy and now skipped via the value === undefined || value === false short-circuit in applySearch.

Evidence

function applySearch(url: URL, params: Record<string, SearchValue>): void {
for (const [key, value] of Object.entries(params)) {
if (value === undefined || value === false) continue;
...
url.searchParams.set(key, value === true ? "true" : value);
}
}

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