Skip to content

feat: add anonRefreshToken to CLI auth flow and enhance session management#1303

Open
mantrakp04 wants to merge 6 commits intodevfrom
anon-cli-auth
Open

feat: add anonRefreshToken to CLI auth flow and enhance session management#1303
mantrakp04 wants to merge 6 commits intodevfrom
anon-cli-auth

Conversation

@mantrakp04
Copy link
Copy Markdown
Collaborator

@mantrakp04 mantrakp04 commented Apr 1, 2026

  • Extended CliAuthAttempt with anonRefreshToken and a migration.
  • CLI POST /auth/cli accepts optional anon_refresh_token (must be an anonymous user's refresh token for the current project).
  • POST /auth/cli/complete supports mode check (anonymous vs none), claim-anon-session (issue tokens for the linked anonymous session), and complete (bind the browser session's refresh token to the attempt). Completing clears anonRefreshToken on the row. We do not merge anonymous account data into the signed-in user (that behavior was removed as a security risk; the anonymous user remains unchanged).
  • Template CLI confirmation page, stack-cli optional STACK_CLI_ANON_REFRESH_TOKEN, SDK/spec updates, and e2e coverage.

Summary by CodeRabbit

  • New Features

    • CLI login supports attaching anonymous sessions and a multi-mode confirm/claim/check flow; CLI tools now surface login codes and remove anon token after use.
    • Added interactive CLI auth demo page and a CLI simulator script.
    • Client libraries: prompt flow accepts an optional anon token and a promptLink(url, loginCode) callback.
  • Tests

    • Expanded end-to-end coverage for anonymous CLI sessions, claim/complete/poll flows, upgrades, and error cases.
  • Documentation

    • Updated prompt CLI docs/spec to describe new options and callback signature.

…ement

- Updated the CliAuthAttempt model to include anonRefreshToken.
- Added migration to support the new anonRefreshToken field.
- Enhanced CLI authentication routes to handle anonymous user sessions, including validation and merging of anonymous user data into authenticated sessions.
- Updated tests to cover new functionality for anonymous sessions and refresh token handling.
Copilot AI review requested due to automatic review settings April 1, 2026 03:26
@vercel
Copy link
Copy Markdown

vercel bot commented Apr 1, 2026

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
stack-auth-hosted-components Ready Ready Preview, Comment Apr 9, 2026 11:35pm
stack-backend Ready Ready Preview, Comment Apr 9, 2026 11:35pm
stack-dashboard Ready Ready Preview, Comment Apr 9, 2026 11:35pm
stack-demo Ready Ready Preview, Comment Apr 9, 2026 11:35pm
stack-docs Ready Ready Preview, Comment Apr 9, 2026 11:35pm
stack-preview-backend Ready Ready Preview, Comment Apr 9, 2026 11:35pm
stack-preview-dashboard Ready Ready Preview, Comment Apr 9, 2026 11:35pm

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Apr 1, 2026

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds nullable anonRefreshToken to CliAuthAttempt, accepts anon_refresh_token on /auth/cli, and implements multi-mode /auth/cli/complete (check / claim-anon-session / complete) with atomic claim logic. SDK, CLI, UI, tests, demos, and Prisma schema/migration updated.

Changes

Cohort / File(s) Summary
Database Schema
apps/backend/prisma/migrations/20260331000000_add_anon_refresh_token_to_cli_auth/migration.sql, apps/backend/prisma/schema.prisma
Added nullable anonRefreshToken column/field to CliAuthAttempt (migration + schema).
Backend Endpoints
apps/backend/src/app/api/latest/auth/cli/route.tsx, apps/backend/src/app/api/latest/auth/cli/complete/route.tsx
/auth/cli accepts optional anon_refresh_token and validates it before creating a tenancy-scoped CliAuthAttempt via raw INSERT. /auth/cli/complete now supports `mode: "check"
E2E Tests
apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts, .../route.test.ts, .../poll.test.ts
Expanded coverage for anon session flows, claim/upgrade/complete behaviors, client accessType polling, negative/error cases, and metadata/isolation assertions.
CLI & Config
packages/stack-cli/src/commands/login.ts, packages/stack-cli/src/lib/config.ts
Added STACK_CLI_ANON_REFRESH_TOKEN config key; login reads optional anon token, passes it to SDK, prints prompt link, and removes anon config on successful login.
SDK Interfaces & Impl
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts, .../client-app-impl.ts
promptCliLogin gains anonRefreshToken option and promptLink(url, loginCode) callback; implementation includes anon_refresh_token in initiation and passes URL+code to callback. Added internal sendRequest/signInWithTokens surfaces.
Frontend Confirmation UI
packages/template/src/components-page/cli-auth-confirm.tsx
Refactored flow to handle check/claim-anon-session/complete modes, auto-complete guard, URL-confirm mutation, intermediate UI states, and adjusted user hook usage.
Docs, Demos & Examples
sdks/spec/src/apps/client-app.spec.md, examples/demo/cli-sim.mjs, examples/demo/src/app/cli-auth-demo/page.tsx
Updated spec for promptCliLogin, added CLI simulator script, and comprehensive demo page implementing the multi-phase CLI auth workflow and persistence.

Sequence Diagram(s)

sequenceDiagram
    participant CLI as CLI (stack)
    participant SDK as StackClientApp
    participant Auth as /auth/cli
    participant DB as Database
    participant Poll as /auth/cli/poll
    participant Complete as /auth/cli/complete

    CLI->>SDK: promptCliLogin({ anonRefreshToken?, promptLink })
    SDK->>Auth: POST /auth/cli { anon_refresh_token? }
    Auth->>DB: validate anon_refresh_token via $queryRaw
    DB-->>Auth: anon token -> anonymous user (or not)
    Auth->>DB: INSERT CliAuthAttempt (anonRefreshToken) RETURNING codes
    Auth-->>SDK: polling_code, login_code, expires_at
    SDK->>CLI: invoke promptLink(url, loginCode)

    CLI->>Poll: poll /auth/cli/poll (polling_code)
    Poll->>DB: SELECT CliAuthAttempt status
    DB-->>Poll: waiting / success / used / expired
    Poll-->>CLI: status

    Browser->>Complete: POST /auth/cli/complete mode:"check" (login_code)
    Complete->>DB: SELECT CliAuthAttempt
    DB-->>Complete: anonRefreshToken present -> cli_session_state:"anonymous"
    Complete-->>Browser: { cli_session_state:"anonymous" }

    Browser->>Complete: POST mode:"claim-anon-session" (login_code)
    Complete->>DB: derive tokens from anonRefreshToken
    DB-->>Complete: access_token, refresh_token
    Complete-->>Browser: tokens

    Browser->>Complete: POST mode:"complete" (login_code, refresh_token)
    Complete->>DB: UPDATE ... WHERE refreshToken IS NULL AND usedAt IS NULL AND expiresAt > NOW() RETURNING *
    DB-->>Complete: success / failure
    Complete-->>Browser: { success: true } or error
Loading
sequenceDiagram
    participant CLI as Stack CLI
    participant Config as Config Module
    participant SDK as StackClientApp
    participant Auth as /auth/cli
    participant Complete as /auth/cli/complete

    CLI->>Config: readConfigValue(STACK_CLI_ANON_REFRESH_TOKEN)
    Config-->>CLI: anon token?
    CLI->>SDK: promptCliLogin({ anonRefreshToken, promptLink })
    SDK->>Auth: POST /auth/cli with anon_refresh_token
    Auth-->>SDK: polling_code, login_code
    SDK->>CLI: promptLink(url, loginCode) or log code
    CLI->>SDK: poll until success
    SDK->>Complete: receives refresh_token
    SDK-->>CLI: return refresh_token
    CLI->>Config: writeConfigValue(STACK_CLI_REFRESH_TOKEN)
    CLI->>Config: removeConfigValue(STACK_CLI_ANON_REFRESH_TOKEN) (if provided)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • Stack CLI #1227: Extends CLI auth to support anonymous refresh tokens and updates CLI/config handling; directly related to anon session and CLI changes.

Suggested reviewers

  • N2D4

Poem

🐇 A rabbit's tiny CLI rhyme
I slipped a token in the grassy row,
Check, claim, complete — three hops to go,
Polling waits while browsers show,
Anonymous hops now merge and glow — hop on, let's go! 🥕

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 9.09% 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 title accurately summarizes the main feature: adding anonRefreshToken support to the CLI auth flow and enhancing session management, which aligns with the substantial changes across database schema, API endpoints, and SDK implementations.
Description check ✅ Passed The description covers the key changes including the new anonRefreshToken field, the three modes for the complete endpoint, the security decision to not merge anonymous data, and mentions of template, CLI, SDK, and e2e coverage; however, the repository's description template only requires a CONTRIBUTING.md reference.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch anon-cli-auth

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.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR extends the CLI authentication flow to support carrying an anonymous CLI session through browser-based authentication, including optionally claiming/merging anonymous user state into an authenticated session.

Changes:

  • Add anonRefreshToken support to CLI auth attempts (schema + migration) and to the JS SDK promptCliLogin flow.
  • Enhance browser confirmation handling to support anonymous-session claiming and post-login auto-completion.
  • Add extensive e2e coverage for CLI auth initiation/poll/complete flows (including anonymous session scenarios) and add demo utilities.

Reviewed changes

Copilot reviewed 17 out of 17 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
sdks/spec/src/apps/client-app.spec.md Updates CLI login spec (new confirm URL, callback signature, anon refresh token support).
packages/template/src/lib/stack-app/apps/interfaces/client-app.ts Extends promptCliLogin options to include anonRefreshToken + updated promptLink signature.
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts Sends anon_refresh_token during CLI auth initiation and surfaces login code to prompt callback/default output.
packages/template/src/components-page/cli-auth-confirm.tsx Enhances confirm page to support anonymous session claiming + auto-complete after sign-in/sign-up.
packages/stack-cli/src/lib/config.ts Adds STACK_CLI_ANON_REFRESH_TOKEN to CLI config key set.
packages/stack-cli/src/commands/login.ts Passes anon refresh token into promptCliLogin and clears it after successful login.
examples/demo/src/app/cli-auth-demo/page.tsx Adds a UI demo page to exercise multiple CLI/browser auth scenarios.
examples/demo/cli-sim.mjs Adds a minimal CLI simulator script for local demo/testing.
apps/e2e/tests/js/restricted-users.test.ts Minor formatting-only change (blank lines).
apps/e2e/tests/backend/endpoints/api/v1/auth/cli/route.test.ts Adds tests for anon_refresh_token validation and client access type initiation.
apps/e2e/tests/backend/endpoints/api/v1/auth/cli/poll.test.ts Adds client access type coverage for polling and full client-cycle test.
apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts Adds extensive coverage for check/claim/complete, anonymous claiming, merges, and edge cases.
apps/backend/src/lib/user-merge.tsx Introduces transactional merge logic from anonymous user → authenticated user, then deletes anonymous user.
apps/backend/src/app/api/latest/auth/cli/route.tsx Accepts/validates anon_refresh_token and persists it on the CLI auth attempt.
apps/backend/src/app/api/latest/auth/cli/complete/route.tsx Adds mode (check/claim-anon-session/complete) and merge-on-complete behavior.
apps/backend/prisma/schema.prisma Adds anonRefreshToken column to CliAuthAttempt.
apps/backend/prisma/migrations/20260331000000_add_anon_refresh_token_to_cli_auth/migration.sql Migration to add anonRefreshToken column.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 1, 2026

Greptile Summary

This PR extends the CLI auth flow to support anonymous sessions: a new anonRefreshToken field is added to CliAuthAttempt, the POST /auth/cli endpoint accepts an optional anonymous refresh token, and POST /auth/cli/complete gains three modes (check, claim-anon-session, complete) to inspect or consume the anonymous session. Merging of anonymous user data into authenticated users was intentionally removed as a security risk. The implementation uses atomic SQL UPDATE … RETURNING guards to prevent replay, error handling in the confirm page is now explicit, and e2e coverage is thorough.

Confidence Score: 5/5

Safe to merge; all findings are P2 style/robustness suggestions that do not block the primary auth path.

All three comments are P2: a stale test-harness description, a Map-vs-plain-object convention, and a low-probability edge case in the claim flow. No P0/P1 issues remain after the previous review round addressed error-handling gaps in the confirm page.

apps/backend/src/app/api/latest/auth/cli/complete/route.tsx (consume-before-validate edge case), packages/stack-cli/src/lib/config.ts (plain object keys)

Vulnerabilities

No significant security issues identified. The atomic UPDATE … WHERE anonRefreshToken = $token AND usedAt IS NULL AND expiresAt > NOW() RETURNING id pattern correctly prevents replay of the one-shot claim. The anon_refresh_token supplied to POST /auth/cli is validated for tenancy ownership and anonymity before being stored. The anonRefreshToken column is cleared on both claim-anon-session and complete, preventing cross-mode token reuse.

Important Files Changed

Filename Overview
apps/backend/src/app/api/latest/auth/cli/complete/route.tsx New multi-mode (check/claim-anon-session/complete) handler using SmartRouteHandler; atomic UPDATEs guard one-shot consumption correctly; minor non-retryable edge case after access-token generation failure.
apps/backend/src/app/api/latest/auth/cli/route.tsx Adds optional anon_refresh_token validation (tenancy, expiry, is_anonymous checks) before storing on CliAuthAttempt row; logic is sound.
packages/template/src/components-page/cli-auth-confirm.tsx Implements check→claim/redirect flow with explicit error handling for non-ok responses; uses runAsynchronouslyWithAlert for async handlers; overall clean.
packages/stack-cli/src/lib/config.ts Adds STACK_CLI_ANON_REFRESH_TOKEN to typed ConfigKey; readConfigJson uses plain object for dynamic keys, risking prototype pollution per project convention.
examples/demo/src/app/cli-auth-demo/page.tsx Internal dev harness for CLI auth flows; test case 5 flow description incorrectly references 'merge anon into real user' which was explicitly removed.
apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts Comprehensive e2e coverage for all three modes (check, claim-anon-session, complete), expiry, replay protection, and metadata isolation.

Sequence Diagram

sequenceDiagram
    participant CLI
    participant API as Backend API
    participant Browser

    CLI->>API: POST /auth/cli (with optional anon token)
    API-->>CLI: polling_code, login_code, expires_at

    CLI->>Browser: Open confirm URL with login_code

    Browser->>API: POST /auth/cli/complete (mode=check)
    API-->>Browser: cli_session_state: anonymous OR none

    alt cli_session_state == anonymous
        Browser->>API: POST /auth/cli/complete (mode=claim-anon-session)
        API-->>Browser: anon user tokens
        Browser->>Browser: signInWithTokens then redirectToSignUp
        Browser->>API: POST /auth/cli/complete (mode=complete, upgraded token)
        API-->>Browser: success
    else user already signed in
        Browser->>API: POST /auth/cli/complete (mode=complete, browser token)
        API-->>Browser: success
    else not signed in
        Browser->>Browser: redirectToSignIn
        Browser->>API: POST /auth/cli/complete (mode=complete, signed-in token)
        API-->>Browser: success
    end

    loop polling
        CLI->>API: POST /auth/cli/poll (polling_code)
        API-->>CLI: waiting OR success with stored token
    end
Loading

Fix All in Claude Code Fix All in Cursor Fix All in Codex

Prompt To Fix All With AI
This is a comment left during a code review.
Path: examples/demo/src/app/cli-auth-demo/page.tsx
Line: 155-159

Comment:
**Stale "merge" description in test case 5**

The flow string says "merge anon into real user" but the PR explicitly removes merging as a security risk — the anonymous user is left unchanged and only the authenticated user's token is bound to the CLI. This description will mislead anyone running the test harness.

```suggestion
  {
    id: 5,
    browser: 'Has real user',
    cli: 'Has anon',
    flow: 'Confirm → complete directly with real user token → CLI gets authenticated token (anon user untouched)',
    setupBrowser: 'keep' as const,
    setupCli: 'anon' as const,
  },
```

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: packages/stack-cli/src/lib/config.ts
Line: 9-14

Comment:
**Plain object used for dynamic config keys**

`JSON.parse` returns a plain object whose prototype chain is traversable; a maliciously crafted `credentials.json` containing keys like `__proto__` can pollute `Object.prototype`. Per project convention, `Map<string, string>` should be used for objects with dynamic keys.

```suggestion
function readConfigJson(): Map<string, string> {
  try {
    const raw = JSON.parse(fs.readFileSync(CONFIG_PATH, "utf-8")) as Record<string, unknown>;
    return new Map(Object.entries(raw).filter((e): e is [string, string] => typeof e[1] === "string"));
  } catch {
    return new Map();
  }
}

function writeConfigJson(data: Map<string, string>): void {
  fs.mkdirSync(path.dirname(CONFIG_PATH), { recursive: true });
  fs.writeFileSync(CONFIG_PATH, JSON.stringify(Object.fromEntries(data), null, 2) + "\n", { mode: 0o600 });
}
```

`readConfigValue`, `writeConfigValue`, and `removeConfigValue` would then use `data.get(key)`, `data.set(key, value)`, and `data.delete(key)` respectively.

**Rule Used:** Use Map<A, B> instead of plain objects when using ... ([source](https://app.greptile.com/review/custom-context?memory=cd0e08f7-0df2-43c8-8c71-97091bba4120))

**Learnt From**
[stack-auth/stack-auth#769](https://github.com/stack-auth/stack-auth/pull/769)
[stack-auth/stack-auth#835](https://github.com/stack-auth/stack-auth/pull/835)
[stack-auth/stack-auth#839](https://github.com/stack-auth/stack-auth/pull/839)
[stack-auth/stack-auth#472](https://github.com/stack-auth/stack-auth/pull/472)
[stack-auth/stack-auth#720](https://github.com/stack-auth/stack-auth/pull/720)
[stack-auth/stack-auth#700](https://github.com/stack-auth/stack-auth/pull/700)
[stack-auth/stack-auth#813](https://github.com/stack-auth/stack-auth/pull/813)

How can I resolve this? If you propose a fix, please make it concise.

---

This is a comment left during a code review.
Path: apps/backend/src/app/api/latest/auth/cli/complete/route.tsx
Line: 239-252

Comment:
**Token consumed before access-token generation is verified**

The atomic UPDATE (line 225–237) nulls out `anonRefreshToken` before `generateAccessTokenFromRefreshTokenIfValid` is called. If the latter returns `null` (e.g. the refresh token was revoked in the narrow window between `getCliAnonymousSession` and the UPDATE), the 400 error is returned but `anonRefreshToken` is permanently NULL in the row — the client can never retry `claim-anon-session`.

Consider generating the access token *before* the atomic UPDATE, or, if generation fails after the UPDATE, restoring `anonRefreshToken` in the error path so the operation remains retryable.

How can I resolve this? If you propose a fix, please make it concise.

Reviews (3): Last reviewed commit: "refactor: enhance CLI authentication flo..." | Re-trigger Greptile

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 7

🧹 Nitpick comments (2)
packages/stack-cli/src/commands/login.ts (1)

30-32: Consider exposing the login code to the user.

The promptLink callback receives both url and loginCode, but only the URL is displayed. Showing the login code could help users verify they're completing authentication for the correct session, improving security UX.

🔧 Optionally display the login code
-      promptLink: (url) => {
-        console.log(`\nPlease visit the following URL to authenticate:\n${url}`);
+      promptLink: (url, loginCode) => {
+        console.log(`\nPlease visit the following URL to authenticate:\n${url}`);
+        console.log(`Login code: ${loginCode}`);
       },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/stack-cli/src/commands/login.ts` around lines 30 - 32, The
promptLink callback currently only prints the URL and ignores the provided
loginCode; update the promptLink implementation (in login.ts) to include the
loginCode alongside the URL in the console output so users can verify the
session (e.g., print both url and loginCode in a clear message), and guard
against missing loginCode by conditionally including it when present to avoid
undefined being shown.
packages/template/src/components-page/cli-auth-confirm.tsx (1)

10-11: Avoid erasing types in this auth handoff.

app as any and err as Error remove the only contracts around the symbol internals and thrown errors, so a shape drift here becomes a runtime auth failure. Give getStackAppInternals() a concrete return type and normalize unknown before calling setError.

As per coding guidelines "Try to avoid the any type. Whenever you need to use any, leave a comment explaining why you're using it and how you can be certain that errors would still be flagged." and "Do NOT use as/any/type casts or anything else to bypass the type system unless you specifically asked the user about it."

Also applies to: 68-69, 123-124

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/components-page/cli-auth-confirm.tsx` around lines 10 -
11, Replace the unchecked casts by defining a concrete interface for the
internals (e.g., StackAppInternals) and a type guard isStackAppInternals(obj:
unknown): obj is StackAppInternals, then change getStackAppInternals(app:
unknown) to return StackAppInternals | undefined and use the guard to safely
access (stackAppInternalsSymbol) instead of app as any; similarly, add a
normalizeError(err: unknown): Error helper or an isError type guard and call it
before setError (replace err as Error) so thrown values are
validated/normalized; update the call sites referenced (the getStackAppInternals
usage and the places that call setError) to use these guards/helpers to avoid
any/ casts.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/src/app/api/latest/auth/cli/route.tsx`:
- Around line 54-65: The error message is misleading when the DB returns null;
update the logic after globalPrismaClient.projectUser.findUnique so it first
checks if user is null and throws a clear StatusError (e.g., "User not found for
provided refresh token" or similar) and only then checks user.isAnonymous and
throws the existing "does not belong to an anonymous user" error; reference the
variables/globalPrismaClient.projectUser.findUnique, user, refreshTokenObj, and
the StatusError throw site to locate where to add the distinct null-check.

In `@examples/demo/src/app/cli-auth-demo/page.tsx`:
- Around line 237-243: doBrowserSignOut currently calls browserUser.signOut()
which triggers the app's after-sign-out redirect and navigates away before the
CLI flow setup; change the call in doBrowserSignOut to prevent that redirect by
using browserUser.signOut with options to either disable automatic redirect
(e.g., redirect: false) or explicitly set the post-sign-out URL to the current
demo path (e.g., callbackUrl or returnTo set to window.location.pathname or
'/cli-auth-demo'), and if you disable redirect, perform any required local state
cleanup and keep the user on the same pathname so the CLI setup can proceed.
- Around line 170-183: The hydration and signup flows drop anonymous sessions
because cliApp.getUser({ includeRestricted: true }) can return null for
anonymous users; update both the useEffect hydration (where loadCliState and
refreshCliAppSession are used) and doCliAnonSignUp to detect when refreshed
response contains partial/anon user info or anonRefreshToken even if getUser
returns null, and populate setCliState from the refreshed payload (e.g.,
refreshed.user / refreshed.partialUser or refreshed.anonRefreshToken + parsed
userId/isAnonymous) instead of relying solely on getUser; reuse the same
partial-user fallback logic in both places so anonymous sessions are persisted
and anonRefreshToken is passed through.
- Around line 342-344: The confirmUrl computation reads window.location.origin
during render which breaks SSR; change it so window is only accessed on the
client (e.g., compute confirmUrl inside a client-only hook/effect or guard with
typeof window !== 'undefined' before using window). Update the code that defines
confirmUrl (the variable that currently depends on loginCode) to fall back to
null during SSR and set the full URL on the client after mount (or use a
useMemo/useState + effect) so rendering/hydration no longer attempts to read
window at build time.

In `@packages/template/src/components-page/cli-auth-confirm.tsx`:
- Around line 100-122: The code treats non-OK responses from postCliAuthComplete
as a benign “not anonymous” path; change it to stop and handle errors instead:
after calling postCliAuthComplete in the "check" flow (variable checkResult) if
!checkResult.ok, abort the flow (e.g., surface an error / redirect to sign-in
with an error) rather than proceeding; likewise for the "claim-anon-session"
call (variable claimResult) if !claimResult.ok, abort immediately instead of
treating it as a fallthrough to sign-in. Locate postCliAuthComplete usage and
update the branches around checkResult and claimResult to explicitly handle
non-OK responses before attempting JSON parsing, calling
getStackAppInternals(...).signInWithTokens, or redirecting to
app.redirectToSignUp/app.redirectToSignIn.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 3006-3008: The payload builder currently uses a truthiness check
for options.anonRefreshToken which omits empty-string values; update the
conditional in the JSON body construction (where expires_in_millis and
anon_refresh_token are set) to include anon_refresh_token when
options.anonRefreshToken is not null or undefined (explicit !== null && !==
undefined) so empty strings are preserved and sent to the backend; adjust the
spread expression that currently reads ...(options.anonRefreshToken ? {
anon_refresh_token: options.anonRefreshToken } : {}) to use this explicit
null/undefined check.

In `@sdks/spec/src/apps/client-app.spec.md`:
- Around line 841-853: The documented poll state is out of sync with the
implementation/tests: update the CLI flow docs (the section describing POST
/api/v1/auth/cli, the polling contract and any examples) to state that poll
responses return { status: "waiting" } instead of { status: "pending" }, and
ensure any references to options.promptLink or options.anonRefreshToken and the
returned refresh token example reflect this "waiting" status so the spec matches
the client implementation and tests.

---

Nitpick comments:
In `@packages/stack-cli/src/commands/login.ts`:
- Around line 30-32: The promptLink callback currently only prints the URL and
ignores the provided loginCode; update the promptLink implementation (in
login.ts) to include the loginCode alongside the URL in the console output so
users can verify the session (e.g., print both url and loginCode in a clear
message), and guard against missing loginCode by conditionally including it when
present to avoid undefined being shown.

In `@packages/template/src/components-page/cli-auth-confirm.tsx`:
- Around line 10-11: Replace the unchecked casts by defining a concrete
interface for the internals (e.g., StackAppInternals) and a type guard
isStackAppInternals(obj: unknown): obj is StackAppInternals, then change
getStackAppInternals(app: unknown) to return StackAppInternals | undefined and
use the guard to safely access (stackAppInternalsSymbol) instead of app as any;
similarly, add a normalizeError(err: unknown): Error helper or an isError type
guard and call it before setError (replace err as Error) so thrown values are
validated/normalized; update the call sites referenced (the getStackAppInternals
usage and the places that call setError) to use these guards/helpers to avoid
any/ casts.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 91e27ad8-a0ef-4bb4-8193-010c8d589930

📥 Commits

Reviewing files that changed from the base of the PR and between df5c588 and c44075c.

📒 Files selected for processing (17)
  • apps/backend/prisma/migrations/20260331000000_add_anon_refresh_token_to_cli_auth/migration.sql
  • apps/backend/prisma/schema.prisma
  • apps/backend/src/app/api/latest/auth/cli/complete/route.tsx
  • apps/backend/src/app/api/latest/auth/cli/route.tsx
  • apps/backend/src/lib/user-merge.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/auth/cli/poll.test.ts
  • apps/e2e/tests/backend/endpoints/api/v1/auth/cli/route.test.ts
  • apps/e2e/tests/js/restricted-users.test.ts
  • examples/demo/cli-sim.mjs
  • examples/demo/src/app/cli-auth-demo/page.tsx
  • packages/stack-cli/src/commands/login.ts
  • packages/stack-cli/src/lib/config.ts
  • packages/template/src/components-page/cli-auth-confirm.tsx
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
  • sdks/spec/src/apps/client-app.spec.md

- Updated user validation logic to throw an error if the user is not found for the provided refresh token.
- Enhanced error handling for anonymous user merges to skip the merge if the user has been upgraded concurrently.
- Adjusted CLI auth demo page to correctly handle anonymous user state and refresh tokens.
- Updated tests to reflect changes in user validation and session management.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
examples/demo/src/app/cli-auth-demo/page.tsx (1)

141-146: ⚠️ Potential issue | 🟠 Major

The browser reset helper still can't produce a clean signed-out setup.

useUser({ includeRestricted: true }) hides anonymous sessions, so doBrowserSignOut() is a no-op for anonymous browsers. When it does find a real user, browserUser.signOut({ redirectUrl: '/cli-auth-demo' }) still performs a full navigation, which aborts the rest of runTestCase(). The sign-out cases therefore either keep stale anon state or reload before CLI setup continues.

Also applies to: 243-249

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@examples/demo/src/app/cli-auth-demo/page.tsx` around lines 141 - 146, The
browser reset currently hides anonymous sessions and triggers a full page
navigation on sign-out; change useUser({ includeRestricted: true }) to include
anonymous sessions (remove the option or set includeRestricted: false) so
doBrowserSignOut can operate on anon sessions, and update
doBrowserSignOut/browserUser.signOut calls to perform a silent sign-out (e.g.,
call browserUser.signOut with options that disable redirect/navigation such as
redirect: false or no redirectUrl, or use browserApp.auth.signOut() if
available) so runTestCase can continue without the page reloading.
🧹 Nitpick comments (1)
apps/backend/src/lib/user-merge.tsx (1)

159-187: Consider batching permission operations for scalability.

The current approach queries authenticatedPermissionIds and processes permissions sequentially per team. While acceptable for typical CLI auth scenarios with few team memberships, this could become slow if users have many teams with many permissions.

For future-proofing, consider batching the delete and update operations. However, given the CLI auth context where large permission sets are unlikely, this is not blocking.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/lib/user-merge.tsx` around lines 159 - 187, Refactor the
per-permission sequential delete/update loop to batch operations: collect ids to
delete and ids to reassign (from membership.teamMemberDirectPermissions) into
arrays, then call tx.teamMemberDirectPermission.deleteMany({ where: { id: { in:
deleteIds } } }) and tx.teamMemberDirectPermission.updateMany({ where: { id: {
in: updateIds } }, data: { projectUserId: options.authenticatedUserId } }) (or
multiple updateMany calls if scopes differ); keep the existing
authenticatedPermissionIds lookup but replace the await-per-permission
delete/update in the loop with population of deleteIds/updateIds and perform the
bulk operations after the loop to improve scalability.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/src/app/api/latest/auth/cli/route.tsx`:
- Line 20: The code currently skips validation for empty anon_refresh_token due
to a truthiness check; change the conditional that reads something like if
(anon_refresh_token) to an explicit null/undefined check like if
(anon_refresh_token != null) so empty strings are passed into the validation
path and will produce an explicit "Invalid anon refresh token" error; update any
related logic (e.g., the validateAnonRefreshToken/anon_refresh_token handling in
route handler) to ensure empty string inputs are treated as invalid and return
the appropriate error response.

In `@examples/demo/src/app/cli-auth-demo/page.tsx`:
- Around line 44-49: The JWT decoding in parseAccessTokenUserSnapshot is using
atob on a base64url payload and inferring anonymity from iss; change it to first
convert the base64url payload to valid base64 (replace '-'->'+' and '_'->'/' and
add '=' padding) before decoding the JSON, and read the actual is_anonymous
boolean claim (e.g., payload.is_anonymous) to set isAnonymous; ensure you still
fall back to payload.userId or payload.sub for userId and handle malformed
tokens safely.

In `@packages/template/src/components-page/cli-auth-confirm.tsx`:
- Line 36: The useUser hook call currently omits anonymous sessions which causes
user to be null for anonymous browsers; update the app.useUser invocation in
cli-auth-confirm.tsx (the const user = app.useUser(...) line) to include or:
"anonymous" in addition to includeRestricted: true so it becomes useUser({
includeRestricted: true, or: "anonymous" }); this will allow the autocomplete
block (the !user gating around lines ~51-67) and handleAuthorize flow to detect
anonymous browser sessions correctly instead of falling through to
redirectToSignIn().

---

Duplicate comments:
In `@examples/demo/src/app/cli-auth-demo/page.tsx`:
- Around line 141-146: The browser reset currently hides anonymous sessions and
triggers a full page navigation on sign-out; change useUser({ includeRestricted:
true }) to include anonymous sessions (remove the option or set
includeRestricted: false) so doBrowserSignOut can operate on anon sessions, and
update doBrowserSignOut/browserUser.signOut calls to perform a silent sign-out
(e.g., call browserUser.signOut with options that disable redirect/navigation
such as redirect: false or no redirectUrl, or use browserApp.auth.signOut() if
available) so runTestCase can continue without the page reloading.

---

Nitpick comments:
In `@apps/backend/src/lib/user-merge.tsx`:
- Around line 159-187: Refactor the per-permission sequential delete/update loop
to batch operations: collect ids to delete and ids to reassign (from
membership.teamMemberDirectPermissions) into arrays, then call
tx.teamMemberDirectPermission.deleteMany({ where: { id: { in: deleteIds } } })
and tx.teamMemberDirectPermission.updateMany({ where: { id: { in: updateIds } },
data: { projectUserId: options.authenticatedUserId } }) (or multiple updateMany
calls if scopes differ); keep the existing authenticatedPermissionIds lookup but
replace the await-per-permission delete/update in the loop with population of
deleteIds/updateIds and perform the bulk operations after the loop to improve
scalability.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: e4d9ca78-1220-4596-9bb5-79936b61b109

📥 Commits

Reviewing files that changed from the base of the PR and between c44075c and 4a6a2e1.

📒 Files selected for processing (7)
  • apps/backend/src/app/api/latest/auth/cli/route.tsx
  • apps/backend/src/lib/user-merge.tsx
  • examples/demo/src/app/cli-auth-demo/page.tsx
  • packages/stack-cli/src/commands/login.ts
  • packages/template/src/components-page/cli-auth-confirm.tsx
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
  • sdks/spec/src/apps/client-app.spec.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • packages/stack-cli/src/commands/login.ts

@mantrakp04 mantrakp04 requested a review from N2D4 April 3, 2026 23:46
@N2D4 N2D4 requested a review from BilalG1 April 7, 2026 16:56
…nctionality

- Replaced direct Prisma queries with raw SQL for fetching anonymous refresh tokens and CLI auth attempts.
- Removed the user merge functionality to enhance security, ensuring anonymous users remain untouched during authentication.
- Updated tests to reflect changes in the handling of anonymous user sessions and authentication flow.
- Improved error handling for invalid login codes and refresh tokens.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
apps/backend/src/app/api/latest/auth/cli/route.tsx (1)

43-43: ⚠️ Potential issue | 🟡 Minor

Treat empty anon_refresh_token values as invalid, not absent.

if (anon_refresh_token) skips validation for "", so this endpoint silently creates a regular CLI auth attempt instead of returning the invalid anon-token error. Please add a tiny regression case for anon_refresh_token: "" when you flip this guard.

Suggested fix
-    if (anon_refresh_token) {
+    if (anon_refresh_token != null) {

As per coding guidelines "Prefer explicit null/undefinedness checks over boolean checks (e.g., foo == null instead of !foo)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/auth/cli/route.tsx` at line 43, The guard
using if (anon_refresh_token) incorrectly treats empty string as absent; change
the conditional to an explicit null/undefined check (e.g., if
(anon_refresh_token == null) or if (anon_refresh_token !== undefined &&
anon_refresh_token !== null) as appropriate) so empty string falls through to
validation, then add an explicit validation that returns the invalid anon-token
error when anon_refresh_token === "" (update the handler that reads
anon_refresh_token in route.tsx); also add a regression test case sending
anon_refresh_token: "" to assert the endpoint returns the invalid anon-token
error.
🧹 Nitpick comments (2)
apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts (2)

167-175: Encode the new dynamic URL parts.

These requests interpolate teamId and userId directly into paths/query strings. The values are UUID-like today, but the repo rule here is to go through urlString or encodeURIComponent() so tests exercise the same safe URL construction path as production code.

As per coding guidelines "Use urlString``/encodeURIComponent()` instead of normal string interpolation for URLs".

Also applies to: 400-418, 438-447, 565-565

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts` around
lines 167 - 175, The test constructs URLs by interpolating dynamic values
directly into strings (see calls to niceBackendFetch that build
`/api/v1/users?team_id=${teamId}` and `/api/v1/users/${cliAnonymousUser.userId}`
where variables like teamId and cliAnonymousUser.userId are used); update these
calls to pass dynamic parts through the project's safe URL helper (use urlString
or wrap with encodeURIComponent()) so the query/path parameters are encoded the
same way as production code—modify the invocations that produce
teamUsersResponse and anonymousUserResponse (and the other occurrences noted) to
encode teamId and userId before embedding them in the URL passed to
niceBackendFetch.

290-396: Prefer inline snapshots for these new 400-path cases.

The new negative tests only pin status and a substring, so body-shape regressions can slip through unnoticed. Inline snapshots would keep these aligned with the rest of this file and lock the contract more tightly.

As per coding guidelines "Prefer .toMatchInlineSnapshot over other selectors in tests".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts` around
lines 290 - 396, Replace the loose substring assertions with inline snapshots so
the 400-path response bodies are pinned: for the tests that
createResponse/claimResponse/completeResponse/checkResponse (the variables named
createResponse, claimResponse, completeResponse, checkResponse in this test
file) change the expect(...status).toBe(400) + expect(...body).toContain(...)
checks to assert the full response body using
expect(response.body).toMatchInlineSnapshot(...) (one snapshot per negative
case) so the exact error payload/shape is locked; keep the status assertions and
generate appropriate inline snapshot strings that reflect the current error
responses for each of the four failing cases.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@apps/backend/src/app/api/latest/auth/cli/complete/route.tsx`:
- Around line 50-52: The CLI auth expiry check uses new Date() while the claim
uses the DB NOW(), causing clock-drift failures; update the code paths (e.g., in
complete route and in getPendingCliAuthAttempt() / the atomic claim logic
referenced around lines 197-208) to use a single clock source by obtaining the
DB time (SELECT NOW() or the DB client's current timestamp) once and comparing
cliAuth.expiresAt against that DB time (or pass the DB time through
getPendingCliAuthAttempt()), then use that same DB-derived timestamp when
deciding to throw the StatusError in the completion handler so both check and
claim use the same clock.
- Around line 183-185: Replace the boolean falsy check on refresh_token with an
explicit null/undefined check: instead of throwing StatusError when
(!refresh_token), check (refresh_token == null) so empty string values are
treated as provided and will progress to the subsequent validation that throws
"Invalid refresh token"; update the conditional that currently throws new
StatusError(400, "refresh_token is required when mode is 'complete'") to use
refresh_token == null to locate the change near the refresh_token usage in
route.tsx.

In `@apps/backend/src/app/api/latest/auth/cli/route.tsx`:
- Around line 87-90: The SQL in route.tsx uses gen_random_uuid() in the INSERT
into "CliAuthAttempt" but pgcrypto is never enabled in migrations; either add a
migration that runs CREATE EXTENSION IF NOT EXISTS pgcrypto; early in the schema
migrations, or stop using raw gen_random_uuid() and instead rely on Prisma's
UUID default (e.g., using `@default`(uuid()) on the CliAuthAttempt.id field) and
remove the raw UUID call in the route; update route.tsx to match whichever
approach you choose and ensure migrations reflect the change.

---

Duplicate comments:
In `@apps/backend/src/app/api/latest/auth/cli/route.tsx`:
- Line 43: The guard using if (anon_refresh_token) incorrectly treats empty
string as absent; change the conditional to an explicit null/undefined check
(e.g., if (anon_refresh_token == null) or if (anon_refresh_token !== undefined
&& anon_refresh_token !== null) as appropriate) so empty string falls through to
validation, then add an explicit validation that returns the invalid anon-token
error when anon_refresh_token === "" (update the handler that reads
anon_refresh_token in route.tsx); also add a regression test case sending
anon_refresh_token: "" to assert the endpoint returns the invalid anon-token
error.

---

Nitpick comments:
In `@apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts`:
- Around line 167-175: The test constructs URLs by interpolating dynamic values
directly into strings (see calls to niceBackendFetch that build
`/api/v1/users?team_id=${teamId}` and `/api/v1/users/${cliAnonymousUser.userId}`
where variables like teamId and cliAnonymousUser.userId are used); update these
calls to pass dynamic parts through the project's safe URL helper (use urlString
or wrap with encodeURIComponent()) so the query/path parameters are encoded the
same way as production code—modify the invocations that produce
teamUsersResponse and anonymousUserResponse (and the other occurrences noted) to
encode teamId and userId before embedding them in the URL passed to
niceBackendFetch.
- Around line 290-396: Replace the loose substring assertions with inline
snapshots so the 400-path response bodies are pinned: for the tests that
createResponse/claimResponse/completeResponse/checkResponse (the variables named
createResponse, claimResponse, completeResponse, checkResponse in this test
file) change the expect(...status).toBe(400) + expect(...body).toContain(...)
checks to assert the full response body using
expect(response.body).toMatchInlineSnapshot(...) (one snapshot per negative
case) so the exact error payload/shape is locked; keep the status assertions and
generate appropriate inline snapshot strings that reflect the current error
responses for each of the four failing cases.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 234bc383-2fc0-4489-a24f-afcf10c983cb

📥 Commits

Reviewing files that changed from the base of the PR and between 4a6a2e1 and 267d4ef.

📒 Files selected for processing (3)
  • apps/backend/src/app/api/latest/auth/cli/complete/route.tsx
  • apps/backend/src/app/api/latest/auth/cli/route.tsx
  • apps/e2e/tests/backend/endpoints/api/v1/auth/cli/complete.test.ts

…ement and error handling

- Integrated users CRUD handlers for better user validation during anonymous session handling.
- Updated SQL queries to ensure proper fetching of refresh tokens and CLI auth attempts from the correct tenancy database.
- Enhanced error handling for invalid refresh tokens and user not found scenarios.
- Improved response schemas for CLI authentication completion to support various response types.
- Updated demo page to utilize sessionStorage for refresh tokens, ensuring better security practices in the development environment.
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Apr 8, 2026

Tip:

Greploops — Automatically fix all review issues by running /greploops in Claude Code. It iterates: fix, push, re-review, repeat until 5/5 confidence.

Use the Greptile plugin for Claude Code to query reviews, search comments, and manage custom context directly from your terminal.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (2)
apps/backend/src/app/api/latest/auth/cli/route.tsx (2)

108-116: Turn the insert result into an explicit invariant.

rows[0] is assumed to exist. If this query ever returns zero rows, the response builder degrades into a generic undefined crash instead of a clear backend error. Guard it before dereferencing.

As per coding guidelines, "Use ?? throwErr(...) instead of non-null assertions or silent fallback values; include explicit error messages."

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/auth/cli/route.tsx` around lines 108 - 116,
The code dereferences rows[0] (assigned to cliAuth) without checking it; change
this to an explicit invariant using the project's throwErr helper (e.g., replace
direct rows[0] use in the route handler that builds the response with cliAuth =
rows[0] ?? throwErr("No CLI auth row returned for polling_code query") ), then
use cliAuth.pollingCode / loginCode / expiresAt as before; this ensures a clear
error is thrown if the query returns zero rows instead of an undefined crash.

67-81: Keep user inside the type system.

let user; drops this value out of compile-time checking, so user.is_anonymous is no longer verified by TypeScript. Returning the value from a small helper/IIFE keeps the error mapping and the user shape typed.

♻️ Proposed refactor
-      let user;
-      try {
-        user = await usersCrudHandlers.adminRead({
-          tenancy,
-          user_id: refreshTokenObj.projectUserId,
-          allowedErrorTypes: [KnownErrors.UserNotFound],
-        });
-      } catch (error) {
-        if (error instanceof KnownErrors.UserNotFound) {
-          throw new StatusError(400, "User not found for provided refresh token");
-        }
-        throw error;
-      }
+      const user = await (async () => {
+        try {
+          return await usersCrudHandlers.adminRead({
+            tenancy,
+            user_id: refreshTokenObj.projectUserId,
+            allowedErrorTypes: [KnownErrors.UserNotFound],
+          });
+        } catch (error) {
+          if (error instanceof KnownErrors.UserNotFound) {
+            throw new StatusError(400, "User not found for provided refresh token");
+          }
+          throw error;
+        }
+      })();
As per coding guidelines, "Code defensively and avoid `any` type; if used, include a comment explaining why the type system fails."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@apps/backend/src/app/api/latest/auth/cli/route.tsx` around lines 67 - 81,
Replace the untyped "let user;" pattern with a typed helper/IIFE that returns
the result of usersCrudHandlers.adminRead so the compiler knows the user shape;
inside the helper call usersCrudHandlers.adminRead({ tenancy, user_id:
refreshTokenObj.projectUserId, allowedErrorTypes: [KnownErrors.UserNotFound] })
and map KnownErrors.UserNotFound to throw new StatusError(400, "User not found
for provided refresh token"), then return the typed user to the outer scope so
subsequent checks like user.is_anonymous are statically type-checked.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@apps/backend/src/app/api/latest/auth/cli/route.tsx`:
- Around line 108-116: The code dereferences rows[0] (assigned to cliAuth)
without checking it; change this to an explicit invariant using the project's
throwErr helper (e.g., replace direct rows[0] use in the route handler that
builds the response with cliAuth = rows[0] ?? throwErr("No CLI auth row returned
for polling_code query") ), then use cliAuth.pollingCode / loginCode / expiresAt
as before; this ensures a clear error is thrown if the query returns zero rows
instead of an undefined crash.
- Around line 67-81: Replace the untyped "let user;" pattern with a typed
helper/IIFE that returns the result of usersCrudHandlers.adminRead so the
compiler knows the user shape; inside the helper call
usersCrudHandlers.adminRead({ tenancy, user_id: refreshTokenObj.projectUserId,
allowedErrorTypes: [KnownErrors.UserNotFound] }) and map
KnownErrors.UserNotFound to throw new StatusError(400, "User not found for
provided refresh token"), then return the typed user to the outer scope so
subsequent checks like user.is_anonymous are statically type-checked.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a0598829-87ba-494d-9c99-185ac92adc06

📥 Commits

Reviewing files that changed from the base of the PR and between 267d4ef and 754ad22.

📒 Files selected for processing (6)
  • apps/backend/src/app/api/latest/auth/cli/complete/route.tsx
  • apps/backend/src/app/api/latest/auth/cli/route.tsx
  • examples/demo/src/app/cli-auth-demo/page.tsx
  • packages/stack-cli/src/commands/login.ts
  • packages/template/src/components-page/cli-auth-confirm.tsx
  • packages/template/src/lib/stack-app/apps/interfaces/client-app.ts
🚧 Files skipped from review as they are similar to previous changes (4)
  • packages/stack-cli/src/commands/login.ts
  • apps/backend/src/app/api/latest/auth/cli/complete/route.tsx
  • examples/demo/src/app/cli-auth-demo/page.tsx
  • packages/template/src/components-page/cli-auth-confirm.tsx

@mantrakp04 mantrakp04 requested a review from N2D4 April 9, 2026 16:47
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts (1)

3060-3062: Use explicit null/undefined check for promptLink callback.

Prefer options.promptLink != null over a truthiness check here for consistency and to avoid relying on implicit coercion.

♻️ Suggested tweak
-    if (options.promptLink) {
+    if (options.promptLink != null) {
       options.promptLink(url, loginCode);
     } else {

As per coding guidelines: "Prefer explicit null/undefinedness checks over boolean checks (e.g., foo == null instead of !foo)".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`
around lines 3060 - 3062, The current truthiness check for the callback uses "if
(options.promptLink)" which can mis-handle falsy but present values; change it
to an explicit null/undefined check using "options.promptLink != null" before
invoking options.promptLink(url, loginCode). Locate the block building url from
loginCode and calling options.promptLink and replace the conditional
accordingly, preserving the same call to options.promptLink(url, loginCode).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts`:
- Around line 3060-3062: The current truthiness check for the callback uses "if
(options.promptLink)" which can mis-handle falsy but present values; change it
to an explicit null/undefined check using "options.promptLink != null" before
invoking options.promptLink(url, loginCode). Locate the block building url from
loginCode and calling options.promptLink and replace the conditional
accordingly, preserving the same call to options.promptLink(url, loginCode).

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: a7e0001c-5d8d-44f5-8726-6d12e2305e23

📥 Commits

Reviewing files that changed from the base of the PR and between 754ad22 and 28003a2.

📒 Files selected for processing (2)
  • apps/backend/prisma/schema.prisma
  • packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts
🚧 Files skipped from review as they are similar to previous changes (1)
  • apps/backend/prisma/schema.prisma

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.

3 participants