Add useCliAuthConfirmation hook and customizable cliAuthConfirm URL target#1388
Conversation
…arget - Extract CLI auth confirmation logic into useCliAuthConfirmation() so users building custom pages can drive the flow via status/error/isLoading/ authorize()/retry() instead of reimplementing the protocol. - Treat cliAuthConfirm as a normal handler URL target: resolve it via resolveHandlerUrls, allow custom URLs, and build the CLI login URL with a new buildCliAuthConfirmUrl() helper so promptCliLogin honors the resolved target. - Move StackContext to its own module so the hook can be unit-tested with a test double without dragging in the full client-app implementation (which trips the compile-time client-version sentinel). - Register cliAuthConfirm in custom-page prompts and the dev tool components tab, and export the new hook + types from the template entry point.
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
📝 WalkthroughWalkthroughThis PR adds a CLI auth confirmation flow: a new Changes
Sequence DiagramsequenceDiagram
participant User as User
participant Browser as Browser/Component
participant Hook as useCliAuthConfirmation<br/>(Hook)
participant ClientApp as ClientApp
participant API as Stack API
participant Auth as Auth System
User->>Browser: Open cliAuthConfirm URL (with/without login_code)
Browser->>Hook: initialize
Hook->>Hook: parse query (login_code?)
alt no login_code
Hook->>Browser: status = "invalid"
Browser->>User: show invalid message
else login_code present
Hook->>Hook: check current user session
alt user signed in
Hook->>API: POST /auth/cli/complete (login_code + refresh_token)
API-->>Hook: success
Hook->>Browser: status = "success"
Browser->>User: show close-browser message
else anonymous
Hook->>API: GET /auth/cli/session/check (login_code)
API-->>Hook: cli_session_state / tokens?
Hook->>API: POST /auth/cli/session/claim-anon (login_code)
API-->>Hook: access_token + refresh_token
Hook->>Auth: signInWithTokens(access, refresh)
Auth-->>Hook: signed in
Hook->>ClientApp: redirectToSignUp({ replace: true }) (set confirmed=true)
ClientApp->>Browser: navigate to sign-up
end
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Possibly related PRs
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
Greptile SummaryThis PR extracts the CLI auth confirmation flow into a reusable Confidence Score: 5/5Safe to merge — no functional bugs found; the double-click race condition from the previous review has been properly addressed with a ref guard. All findings are P2 (minor API surface concern). The core logic — state machine transitions, ref-based de-duplication, StackContext extraction, and URL resolution — is correct. Tests cover the critical paths. No files require special attention. Important Files Changed
Sequence DiagramsequenceDiagram
participant CLI as CLI Tool
participant Browser
participant Hook as useCliAuthConfirmation
participant API as /auth/cli/complete
CLI->>API: POST (init) → polling_code, login_code
CLI->>Browser: Open buildCliAuthConfirmUrl(cliAuthConfirmUrl, appUrl, login_code)
Browser->>Hook: mount — reads login_code from URL
alt User not signed in
Browser->>Hook: user clicks Authorize
Hook->>API: POST {login_code, mode:"check"}
API-->>Hook: {cli_session_state}
alt anonymous session
Hook->>API: POST {login_code, mode:"claim-anon-session"}
API-->>Hook: {access_token, refresh_token}
Hook->>Browser: signInWithTokens + markUrlConfirmed
Hook->>Browser: redirectToSignUp (status→redirecting)
Browser->>Hook: remount — confirmed=true, user present
Hook->>Hook: autoCompleteRef guard fires
else no session
Hook->>Browser: markUrlConfirmed + redirectToSignIn (status→redirecting)
Browser->>Hook: remount — confirmed=true, user present
end
end
Hook->>Hook: auto-complete effect runs (confirmed & user)
Hook->>API: POST {login_code, refresh_token}
API-->>Hook: 200 OK
Hook->>Browser: status → success
CLI->>API: poll → session completed
Prompt To Fix All With AIThis is a comment left during a code review.
Path: packages/template/src/components-page/cli-auth-confirm.tsx
Line: 60-67
Comment:
**`loginCode` in public state type is unnecessary API surface**
`loginCode: string | null` is exposed on `CliAuthConfirmationState` but is never consumed by `CliAuthConfirmation` and is not needed by any of the custom-page prompt examples in `page-component-versions.ts`. The hook already handles all protocol details internally (`authorize()`, `retry()`, etc.), so exposing the raw code risks encouraging custom pages to re-implement protocol steps directly.
Consider dropping `loginCode` from the return type; it can always be added back later if a concrete use case arises.
How can I resolve this? If you propose a fix, please make it concise.Reviews (2): Last reviewed commit: "Enhance CLI auth confirmation handling a..." | Re-trigger Greptile |
There was a problem hiding this comment.
Pull request overview
This PR improves the CLI auth UX/customizability in the template SDK by extracting the browser-side confirmation protocol into a reusable hook and by making the cliAuthConfirm page a first-class configurable handler URL target.
Changes:
- Added
useCliAuthConfirmation()(with exported types) and refactoredCliAuthConfirmationto consume it. - Registered
cliAuthConfirmas a configurable handler URL target and updatedpromptCliLogin()to build its login URL from the resolved target viabuildCliAuthConfirmUrl(). - Extracted
StackContextinto its own module to support unit testing hooks/components with a simple test double.
Reviewed changes
Copilot reviewed 15 out of 15 changed files in this pull request and generated 1 comment.
Show a summary per file
| File | Description |
|---|---|
| sdks/spec/src/apps/client-app.spec.md | Updates the CLI login URL construction spec to use resolved urls.cliAuthConfirm. |
| packages/template/src/providers/stack-provider-client.tsx | Switches to importing StackContext from the new dedicated module. |
| packages/template/src/providers/stack-context.tsx | New client module exporting StackContext for provider + tests. |
| packages/template/src/lib/stack-app/url-targets.ts | Adds cliAuthConfirm handler resolution and buildCliAuthConfirmUrl() helper. |
| packages/template/src/lib/stack-app/url-targets.test.ts | Adds coverage for cliAuthConfirm resolution + URL building helper. |
| packages/template/src/lib/stack-app/apps/implementations/client-app-impl.ts | Updates promptCliLogin() to use buildCliAuthConfirmUrl(); adds redirectToCliAuthConfirm(). |
| packages/template/src/lib/hooks.tsx | Updates StackContext import location and makes stack-app imports type-only. |
| packages/template/src/index.ts | Exports useCliAuthConfirmation and related types. |
| packages/template/src/dev-tool/dev-tool-core.ts | Adds cliAuthConfirm to the dev-tool components tab. |
| packages/template/src/components-page/stack-handler-client.tsx | Ensures handler routing treats cliAuthConfirm as a handler route. |
| packages/template/src/components-page/cli-auth-confirm.tsx | Introduces the hook + refactors component to use it; improves response parsing. |
| packages/template/src/components-page/cli-auth-confirm.test.tsx | Adds unit tests for the new hook behavior. |
| packages/stack-shared/src/interface/page-component-versions.ts | Registers cliAuthConfirm custom-page prompt and example using the new hook. |
| packages/stack-shared/src/interface/handler-urls.ts | Adds cliAuthConfirm to handler URL target types. |
| claude/CLAUDE-KNOWLEDGE.md | Documents the intended DX and testing approach for the new flow. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
- Introduced a mechanism to ignore duplicate authorization clicks before React re-renders, ensuring that only one request is sent when the authorize button is clicked multiple times. - Added a new test case to validate this behavior, confirming that the request is sent only once despite multiple clicks. - Updated the `useCliAuthConfirmation` hook to manage the authorization state more effectively, preventing re-authorization while a request is in progress. - Improved error handling for missing login codes in the authorization process. - Documented the app URL usage in the `buildCliAuthConfirmUrl` function for clarity on relative URL handling.
There was a problem hiding this comment.
🧹 Nitpick comments (2)
packages/template/src/components-page/cli-auth-confirm.tsx (2)
41-50: Optional: drop theas keyof typeof datacast.After narrowing to
objectand verifyingfieldName in data, a singleRecord<string, unknown>cast reads cleaner than thekeyof typeof dataform (which is effectively a no-op cast sincefieldNameisn't statically known). Functionally identical, just more honest about what's happening.♻️ Optional simplification
function getObjectField(data: unknown, fieldName: string): unknown { - return typeof data === "object" && data !== null && fieldName in data - ? data[fieldName as keyof typeof data] - : undefined; + if (typeof data !== "object" || data === null) return undefined; + if (!(fieldName in data)) return undefined; + return (data as Record<string, unknown>)[fieldName]; }As per coding guidelines: "Do NOT use
as/any/type casts or anything else like that to bypass the type system." — strictly speaking neither form fully avoids a cast here, so feel free to ignore if the current form is preferred.🤖 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 41 - 50, The code uses a verbose cast `fieldName as keyof typeof data` inside getObjectField; replace that with an index on a clearer shape by casting data to Record<string, unknown> after the runtime checks — e.g. use (data as Record<string, unknown>)[fieldName] — and keep getStringField unchanged except it should call the updated getObjectField; this removes the `keyof typeof data` cast and makes the intent clearer while preserving the runtime `fieldName in data` guard.
86-95: Prefer explicit== nullchecks over truthiness for these typed values.Across the hook, several guards use truthiness on values whose types are
T | null/string | undefined(!loginCode,!user,!refreshToken,!accessToken). Per the coding guideline, unless clearly equivalent from types, prefer explicit nullishness checks — the difference matters for, e.g., empty-string tokens that would currently be treated the same as missing tokens with a generic "did not return tokens" error rather than something more diagnostic.♻️ Suggested tightening
const completeWithCurrentUser = useCallback(async () => { - if (!loginCode) { + if (loginCode == null) { throw new Error("Missing login code in URL parameters"); } - if (!user) { + if (user == null) { throw new Error("Cannot complete CLI authorization without a signed-in user"); } const refreshToken = (await user.currentSession.getTokens()).refreshToken; - if (!refreshToken) { + if (refreshToken == null) { throw new Error("Could not retrieve session token"); } await completeCliAuthWithRefreshToken(app, loginCode, refreshToken); }, [app, loginCode, user]); @@ - if (!confirmed || !user || autoCompleteRef.current) { + if (!confirmed || user == null || autoCompleteRef.current) { return; } @@ - if (!accessToken || !refreshToken) { + if (accessToken == null || refreshToken == null) { throw new Error("Anonymous CLI session claim did not return tokens"); }As per coding guidelines: "Unless very clearly equivalent from types, prefer explicit null/undefinedness checks over boolean checks, eg.
foo == nullinstead of!foo".Also applies to: 100-100, 154-156
🤖 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 86 - 95, Replace truthiness guards with explicit nullish checks: change conditions like `if (!loginCode)`, `if (!user)`, `if (!refreshToken)`, and `if (!accessToken)` to use `== null` (e.g., `if (loginCode == null)`) so you only treat null/undefined as missing and allow empty strings to be distinguished; update the error checks in the same block where `loginCode`, `user`, `refreshToken`, and `accessToken` are validated (the guards around retrieving `user.currentSession.getTokens()` and subsequent token usage) to use these `== null` checks and keep the existing error messages.
🤖 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/components-page/cli-auth-confirm.tsx`:
- Around line 41-50: The code uses a verbose cast `fieldName as keyof typeof
data` inside getObjectField; replace that with an index on a clearer shape by
casting data to Record<string, unknown> after the runtime checks — e.g. use
(data as Record<string, unknown>)[fieldName] — and keep getStringField unchanged
except it should call the updated getObjectField; this removes the `keyof typeof
data` cast and makes the intent clearer while preserving the runtime `fieldName
in data` guard.
- Around line 86-95: Replace truthiness guards with explicit nullish checks:
change conditions like `if (!loginCode)`, `if (!user)`, `if (!refreshToken)`,
and `if (!accessToken)` to use `== null` (e.g., `if (loginCode == null)`) so you
only treat null/undefined as missing and allow empty strings to be
distinguished; update the error checks in the same block where `loginCode`,
`user`, `refreshToken`, and `accessToken` are validated (the guards around
retrieving `user.currentSession.getTokens()` and subsequent token usage) to use
these `== null` checks and keep the existing error messages.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 8259deea-f99f-4818-8435-0fabdb67224d
📒 Files selected for processing (3)
packages/template/src/components-page/cli-auth-confirm.test.tsxpackages/template/src/components-page/cli-auth-confirm.tsxpackages/template/src/lib/stack-app/url-targets.ts
🚧 Files skipped from review as they are similar to previous changes (1)
- packages/template/src/components-page/cli-auth-confirm.test.tsx
Summary
useCliAuthConfirmation()hook (status / error / isLoading / authorize / retry) so custom pages don't have to reimplement the protocol;CliAuthConfirmationnow consumes the hook.cliAuthConfirma first-class handler URL target — resolved viaresolveHandlerUrls, customizable per project, and used bypromptCliLoginthrough a newbuildCliAuthConfirmUrl()helper.StackContextto its own module so the hook can be unit-tested with a test double without tripping the client-version sentinel; registercliAuthConfirmin custom-page prompts and the dev-tool components tab; export the hook + types from@stackframe/stack.Test plan
pnpm typecheckpnpm lintpnpm --filter @stackframe/stack test cli-auth-confirm url-targets/handler/cli-auth-confirmflow + a project with a customcliAuthConfirmURLSummary by CodeRabbit
New Features
Tests