Skip to content

feat: integrate Cloudflare Turnstile CAPTCHA for authentication security#4396

Merged
perkinsjr merged 3 commits intomainfrom
captcha-on-login
Nov 24, 2025
Merged

feat: integrate Cloudflare Turnstile CAPTCHA for authentication security#4396
perkinsjr merged 3 commits intomainfrom
captcha-on-login

Conversation

@perkinsjr
Copy link
Member

What does this PR do?

This commit implements CAPTCHA protection using Cloudflare Turnstile across all authentication flows that use Radar to allow "Challenged" requests to show they are indeed a human before sending the OTP.

Type of change

  • Bug fix (non-breaking change which fixes an issue)
  • Chore (refactoring code, technical debt, workflow improvements)
  • Enhancement (small improvements)
  • New feature (non-breaking change which adds functionality)
  • Breaking change (fix or feature that would cause existing functionality to not work as expected)
  • This change requires a documentation update

How should this be tested?

Testing is a bit of a challenge. I have been testing this by:

  1. Using a VPN to login from a country like Netherlands which gets flagged more often.
  2. Running the playwright (https://github.com/mcstepp/unkey-playwright) 10x
  3. Then manually attempt a sign up to trigger this.

I was unable to trigger sign in because Radar would approve it due to the nature of cred stuffing being the motivator for radar blocks, and we don't use passwords.

Checklist

Required

  • Filled out the "How to test" section in this PR
  • Read Contributing Guide
  • Self-reviewed my own code
  • Commented on my code in hard-to-understand areas
  • Ran pnpm build
  • Ran pnpm fmt
  • Ran make fmt on /go directory
  • Checked for warnings, there are none
  • Removed all console.logs
  • Merged the latest changes from main onto my branch with git pull origin main
  • My changes don't cause any responsiveness issues

Appreciated

  • If a UI change was made: Added a screen recording or screenshots to this PR
  • Updated the Unkey Docs if changes were necessary

This commit implements comprehensive CAPTCHA protection using Cloudflare Turnstile
across all authentication flows to enhance security against automated attacks and
bot abuse.

Key Changes:
- Add Turnstile verification component with dark theme and loading states
- Integrate CAPTCHA challenges into sign-in and sign-up flows
- Update authentication hooks (useSignIn, useSignUp) to handle CAPTCHA tokens
- Modify auth actions to validate Turnstile tokens on the server side
- Enhance WorkOS and local authentication providers with CAPTCHA support
- Update auth types to include CAPTCHA token handling
- Add environment configuration for Turnstile site key
- Improve loading states and user experience during verification
- Add proper error handling for CAPTCHA failures
- Update invitation acceptance flows with CAPTCHA protection

Technical Implementation:
- Uses @marsidev/react-turnstile for React integration
- Implements token validation in server-side auth actions
- Adds proper TypeScript types for CAPTCHA-enabled auth flows
- Maintains backward compatibility with existing auth flows
- Includes cumulative layout shift (CLS) optimizations for better UX

Security Benefits:
- Protects against automated sign-up abuse
- Prevents credential stuffing attacks on sign-in
- Reduces spam account creation
- Maintains legitimate user accessibility with minimal friction

Files Modified:
- Authentication components and hooks (29 files)
- Server-side auth actions and routes
- Type definitions and environment configuration
- Package dependencies and build configuration
@changeset-bot
Copy link

changeset-bot bot commented Nov 24, 2025

⚠️ No Changeset found

Latest commit: e1fcb02

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

This PR includes no changesets

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

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

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

@vercel
Copy link

vercel bot commented Nov 24, 2025

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

Project Deployment Preview Comments Updated (UTC)
dashboard Ready Ready Preview Comment Nov 24, 2025 6:54pm
1 Skipped Deployment
Project Deployment Preview Comments Updated (UTC)
engineering Ignored Ignored Preview Nov 24, 2025 6:54pm

@coderabbitai
Copy link
Contributor

coderabbitai bot commented Nov 24, 2025

📝 Walkthrough

Walkthrough

Adds Cloudflare Turnstile verification to auth flows (component, actions, hooks, types, env and dependency), introduces optional bypassRadar flags in auth providers, and removes many console.error/console.warn logs across server and client code while keeping core control flow unchanged.

Changes

Cohort / File(s) Summary
Turnstile component & dependency
apps/dashboard/components/auth/turnstile-challenge.tsx, apps/dashboard/package.json
New TurnstileChallenge React component; adds dependency @marsidev/react-turnstile.
Auth actions: Turnstile verification
apps/dashboard/app/auth/actions.ts
Added verifyTurnstileToken() and verifyTurnstileAndRetry() to validate Turnstile tokens and retry auth actions; replaced several error logs with silent handling.
Hooks: sign-in / sign-up Turnstile integration
apps/dashboard/app/auth/hooks/useSignIn.ts, apps/dashboard/app/auth/hooks/useSignUp.ts
Added isPendingTurnstileChallenge guard and handleTurnstileVerification; integrated Turnstile challenge detection; exposed new helpers and updated handler return types.
Sign-in / Sign-up UI changes
apps/dashboard/app/auth/sign-in/email-signin.tsx, apps/dashboard/app/auth/sign-in/email-code.tsx, apps/dashboard/app/auth/sign-in/oauth-signin.tsx, apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx, apps/dashboard/app/auth/sign-up/email-signup.tsx, apps/dashboard/app/auth/sign-up/email-code.tsx, apps/dashboard/app/auth/sign-up/[[...sign-up]]/page.tsx
Added Turnstile flow handling, client-side validation, explicit form submit handling, and removed console logging in multiple catch paths; new Turnstile state/handlers and UI rendering added.
API routes & invitation handlers
apps/dashboard/app/(app)/api/auth/refresh/route.ts, apps/dashboard/app/api/auth/accept-invitation/route.ts, apps/dashboard/app/api/auth/invitation/route.ts, apps/dashboard/components/auth/post-auth-invitation-handler.tsx
Removed console.error logs from catch blocks; one catch renamed to _error while response still references error (possible bug).
Auth provider implementations & types
apps/dashboard/lib/auth/types.ts, apps/dashboard/lib/auth/base-provider.ts, apps/dashboard/lib/auth/workos.ts, apps/dashboard/lib/auth/local.ts
Added PendingTurnstileResponse and AuthErrorCode.RADAR_CHALLENGE_REQUIRED; extended EmailAuthResult; added optional bypassRadar?: boolean to sign-in/sign-up signatures; simplified radar/error handling and removed many console logs.
Sessions, cookies, get-auth
apps/dashboard/lib/auth/sessions.ts, apps/dashboard/lib/auth/cookies.ts, apps/dashboard/lib/auth/get-auth.ts
Silenced multiple catch logs (renamed error vars to underscore-prefixed); removed cookie deletion logging; minor signature formatting changes.
Env & build config
apps/dashboard/lib/env.ts, turbo.json
Added NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY and CLOUDFLARE_TURNSTILE_SECRET_KEY to env schema; turbo build env updated to include Turnstile site key and removed two Clerk server envs.
Examples / demos
apps/engineering/content/design/components/search/llm-search.examples.tsx, apps/engineering/content/design/components/toaster.example.tsx
Replaced console logging callbacks in examples with no-ops.
Tests
apps/dashboard/lib/auth/tests/check-radar.test.ts
Removed two tests that asserted Radar challenge behavior for signup/signin; remaining tests for block/allow and failures stay.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant UI as SignIn/SignUp UI
    participant Hook as useSignIn/useSignUp
    participant Actions as auth/actions
    participant Server as Auth Provider
    participant Turnstile as Cloudflare Turnstile

    User->>UI: submit email
    UI->>Hook: handleSignInViaEmail / handleSignUpViaEmail
    Hook->>Actions: signIn/signUp(email, client metadata)
    Actions->>Server: signInViaEmail / signUpViaEmail (radar check)
    alt Server returns RADAR_CHALLENGE_REQUIRED
        Server-->>Actions: PendingTurnstileResponse
        Actions-->>Hook: PendingTurnstileResponse
        Hook-->>UI: render TurnstileChallenge
        User->>Turnstile: solve widget
        Turnstile-->>UI: token
        UI->>Hook: handleTurnstileVerification(token, challengeParams)
        Hook->>Actions: verifyTurnstileAndRetry(token, challengeParams)
        Actions->>Server: signInViaEmail/signUpViaEmail (bypassRadar: true)
        Server-->>Actions: success
        Actions-->>Hook: success
        Hook-->>UI: proceed (navigation/state)
    else Server allows / success
        Server-->>Actions: success
        Actions-->>Hook: success
        Hook-->>UI: proceed
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Areas requiring extra attention:

  • verifyTurnstileAndRetry() logic in apps/dashboard/app/auth/actions.ts and its retry mapping to challengeParams.action.
  • Hook return-type changes and state transitions in useSignIn.ts and useSignUp.ts.
  • Potential undefined variable in apps/dashboard/app/api/auth/accept-invitation/route.ts where error was renamed to _error but error is still referenced.
  • Correct propagation and usage of bypassRadar across workos.ts, base-provider.ts, local.ts, and callers.
  • Turnstile component behavior when env keys are missing and client integration in email-signin/email-signup components.
  • Tests changed/removed in apps/dashboard/lib/auth/tests/check-radar.test.ts — ensure test coverage matches intended runtime semantics.

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings, 1 inconclusive)
Check name Status Explanation Resolution
Linked Issues check ⚠️ Warning The PR description states 'Fixes # (issue)' but provides no actual issue number, which violates the template requirement to reference a tracking issue. Add a reference to the GitHub issue that this PR addresses (e.g., 'Fixes #1234') to establish traceability and context for the CAPTCHA implementation.
Docstring Coverage ⚠️ Warning Docstring coverage is 30.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Out of Scope Changes check ❓ Inconclusive Beyond Turnstile integration, the PR removes console.error logs across 15+ files in logging cleanup. While justified by the PR checklist ('Removed all console.logs'), these changes are tangential to the main feature and could be split into a separate refactoring PR. Consider whether extensive logging cleanup should be a separate PR to isolate the core Turnstile feature changes and simplify review; if kept, document the rationale for logging removal.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely describes the main feature: integration of Cloudflare Turnstile CAPTCHA for authentication security. It accurately reflects the primary change across the changeset.
Description check ✅ Passed The PR description covers most required sections: purpose (Turnstile CAPTCHA integration), type of change (new feature), testing approach (VPN, Playwright, manual testing), and completed checklist items. However, it lacks the linked issue reference.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch captcha-on-login

📜 Recent review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 02cc6bb and e1fcb02.

📒 Files selected for processing (1)
  • apps/dashboard/lib/auth/tests/check-radar.test.ts (0 hunks)
💤 Files with no reviewable changes (1)
  • apps/dashboard/lib/auth/tests/check-radar.test.ts
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Test Packages / Test
  • GitHub Check: Test Dashboard / Test Dashboard
  • GitHub Check: Analyze (javascript-typescript)

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.

@github-actions
Copy link
Contributor

github-actions bot commented Nov 24, 2025

Thank you for following the naming conventions for pull request titles! 🙏

Copy link
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

🧹 Nitpick comments (13)
apps/dashboard/lib/auth/sessions.ts (1)

154-167: Consider minimal observability for swallowed session errors

The underscore-prefixed _refreshError, _validationError, and _error make it explicit that errors are ignored, but with all failures collapsing to { session: null, headers }, diagnosing intermittent auth issues may be harder. Consider wiring these branches into your structured logging or metrics system (even at a low verbosity level) so you can distinguish refresh failures from validation errors without reintroducing noisy console logs.

apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx (1)

88-92: Consider preserving error logging for debugging.

While the error is properly handled via setError() to show the user a message, completely suppressing the error in the catch block makes debugging production issues more difficult. Consider preserving some minimal logging (even if just during development) to help trace automatic org selection failures.

-      .catch((_err) => {
+      .catch((err) => {
+        if (process.env.NODE_ENV === 'development') {
+          console.error('Auto org selection failed:', err);
+        }
         setError("Failed to automatically sign in. Please select your workspace.");
apps/dashboard/components/auth/turnstile-challenge.tsx (2)

57-60: Preserve original error details for debugging.

The onError callback receives an error from Turnstile but discards it, creating a generic error instead. This loses potentially valuable debugging information about why Turnstile verification failed.

 onError={(_error) => {
   setIsWidgetLoading(false);
-  onError(new Error("Turnstile verification failed"));
+  onError(_error || new Error("Turnstile verification failed"));
 }}

40-47: Add accessibility attributes to loading overlay.

The loading overlay should include ARIA attributes to properly communicate the loading state to screen readers.

 {isWidgetLoading && (
-  <div className="absolute inset-0 flex items-center justify-center bg-gray-800/90 backdrop-blur-sm rounded border border-gray-600 z-10">
+  <div 
+    className="absolute inset-0 flex items-center justify-center bg-gray-800/90 backdrop-blur-sm rounded border border-gray-600 z-10"
+    role="status"
+    aria-live="polite"
+    aria-label="Loading verification widget"
+  >
     <div className="flex items-center space-x-2">
       <div className="animate-spin h-4 w-4 border-2 border-gray-400 border-t-white rounded-full" />
       <span className="text-sm text-gray-300">Loading verification...</span>
apps/dashboard/app/auth/actions.ts (1)

86-213: Error handling in verifyAuthCode now fully user-facing, but inner errors are completely silent

The outer try/catch now always maps unexpected failures to an UNKNOWN_ERROR response, and all inner catch (_error) blocks (invitation/org lookup and auto-selection) silently swallow errors and fall back to the original result.

Behavior-wise this is fine for UX (you don’t block login on secondary failures), but you lose observability for real bugs in invitation/org flows. Consider routing these _errors through a structured logger (at debug/info level) rather than dropping them entirely, e.g.:

-      } catch (_error) {
-        // Fall through to return the original result if auto-selection fails
-      }
+      } catch (error) {
+        // Fall through to return the original result if auto-selection fails
+        logger.debug?.("auto-org-selection failed", { error });
+      }

Same pattern could apply to the other invitation/org-related catch (_error) blocks.

apps/dashboard/app/auth/hooks/useSignIn.ts (2)

88-118: isAuthErrorResponse type guard is now used with EmailAuthResult—consider widening its parameter type

handleSignInViaEmail now calls:

const result = await signInViaEmail(email); // EmailAuthResult
...
if (isAuthErrorResponse(result)) {
  ...
}

but isAuthErrorResponse is declared as:

function isAuthErrorResponse(result: VerificationResult): result is AuthErrorResponse;

Given EmailAuthResult and VerificationResult are different unions, this can be awkward for TypeScript and may require casts.

You could make the guard reusable across both flows by widening its parameter type, e.g.:

-function isAuthErrorResponse(result: VerificationResult): result is AuthErrorResponse {
+function isAuthErrorResponse(
+  result: EmailAuthResult | VerificationResult,
+): result is AuthErrorResponse {

(or just unknown plus a runtime check) so it cleanly supports both email auth and verification flows.


59-83: Silencing errors in checkAuthStatus is acceptable here but may hide intermittent issues

The outer try now has catch (_err) { /* Ignore auth status check errors */ }, so any failure reading orgs or PENDING_SESSION_COOKIE just results in loading eventually becoming false.

Given this is only used to drive UX hints (org selector and “pending auth” indicator), failing open is reasonable. If you see odd behavior around org selection in the wild, consider emitting a low-level log here rather than fully ignoring.

apps/dashboard/app/auth/sign-up/email-signup.tsx (1)

105-124: Ensure handleTurnstileVerification has access to the user’s name data for sign‑up retries

On Turnstile success you call:

const result = await handleTurnstileVerification(token, turnstileChallenge);
if (result?.success) {
  setTurnstileChallenge(null);
  setVerification(true);
} else {
  toast.error("Verification failed. Please try again.");
}

But verifyTurnstileAndRetry’s sign‑up path expects userData (firstName/lastName) when challengeParams.action === "sign-up".

If the useSignUp hook’s handleTurnstileVerification isn’t already pulling first/last name from its own state/context and passing them through as userData, sign-ups that go through a Turnstile challenge may fail with “Invalid challenge parameters” or lose name information.

Either:

  • Pass the names explicitly here, e.g.:
await handleTurnstileVerification(token, turnstileChallenge, { firstName, lastName });

or

  • Confirm that useSignUp maintains and forwards userData internally.
apps/dashboard/app/auth/sign-in/email-signin.tsx (1)

58-73: Turnstile success flow is solid; consider surfacing a small error on Turnstile failure

On success:

  • You guard against a missing turnstileChallenge.
  • Call handleTurnstileVerification, then clear the challenge and mark "email" as lastUsed.
  • isTurnstileLoading ensures the Turnstile UI can show a loading state.

On error, handleTurnstileError just clears the challenge:

const handleTurnstileError = () => {
  setTurnstileChallenge(null);
};

Functionally this is fine—the user is dropped back to the email form—but you might consider a subtle message (toast or inline) so users know why the challenge disappeared, e.g., “Verification failed, please try again.”

Not strictly required, but could improve UX.

Also applies to: 79-98

apps/dashboard/app/auth/hooks/useSignUp.ts (2)

33-42: Turnstile retry flow wiring is correct and safely guarded by userData checks

handleSignUpViaEmail updates userData before calling signUpViaEmail, which ensures userData is present when a Radar challenge occurs. handleTurnstileVerification then validates firstName/lastName and delegates to verifyTurnstileAndRetry with { email, challengeParams } from the server response and { firstName, lastName } from local state, which matches the expected signature in apps/dashboard/app/auth/actions.ts and the WorkOS provider’s signUpViaEmail with bypassRadar: true. This is aligned with the intended “only bypass Radar after passing Turnstile” pattern.

Also applies to: 44-63


85-99: Resend flow is sound; consider whether the detailed error message should be user‑facing

Validating userData.email before calling resendAuthCode and returning an EmailAuthResult is consistent with the provider’s resendAuthCode contract. Wrapping failures in a new Error that includes the email and original error message is reasonable, but if this message is surfaced directly to end‑users, you may want to switch to a more generic string to avoid leaking internal error details (and the full email) outside the UI context.

apps/dashboard/lib/auth/workos.ts (2)

174-210: Session refresh and org switch logic are straightforward; double‑check TTL alignment

Both refreshSession and switchOrg now:

  • Load the sealed session once via userManagement.loadSealedSession.
  • Call session.refresh(...) and require authenticated && session && sealedSession to proceed.
  • Compute expiresAt as “now + 7 days” using setDate.
  • Throw explicit errors when the refresh result is malformed or unauthenticated.

This is clear and defensive. The only thing worth confirming is that this 7‑day expiresAt matches the cookie TTL configured in getAuthCookieOptions; otherwise you could get subtle mismatches between server‑side session validity and browser cookie lifetime.

Also applies to: 317-351


169-171: Silent fallbacks on provider errors are reasonable but reduce debuggability

Changing several methods to swallow errors and return safe defaults:

  • validateSession{ isValid: false, shouldRefresh: false } on error.
  • getUser/findUsernull on provider failure.
  • getInvitationList[] + empty metadata on failure.
  • getInvitationnull on failure.
  • getSignOutUrlnull on failure.

This is acceptable from a UX perspective (fail softly instead of crashing), especially in read‑only/listing operations. The tradeoff is reduced visibility into upstream WorkOS issues. If debugging becomes harder in practice, consider adding non‑console logging (e.g., structured logs or Sentry) in these catch blocks later without changing the public behavior.

Also applies to: 225-227, 244-246, 529-534, 546-548, 879-880

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9edc879 and ee52ea1.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (28)
  • apps/dashboard/app/(app)/api/auth/refresh/route.ts (0 hunks)
  • apps/dashboard/app/api/auth/accept-invitation/route.ts (2 hunks)
  • apps/dashboard/app/api/auth/invitation/route.ts (1 hunks)
  • apps/dashboard/app/auth/actions.ts (12 hunks)
  • apps/dashboard/app/auth/hooks/useSignIn.ts (6 hunks)
  • apps/dashboard/app/auth/hooks/useSignUp.ts (2 hunks)
  • apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx (3 hunks)
  • apps/dashboard/app/auth/sign-in/email-code.tsx (2 hunks)
  • apps/dashboard/app/auth/sign-in/email-signin.tsx (3 hunks)
  • apps/dashboard/app/auth/sign-in/oauth-signin.tsx (1 hunks)
  • apps/dashboard/app/auth/sign-up/[[...sign-up]]/page.tsx (1 hunks)
  • apps/dashboard/app/auth/sign-up/email-code.tsx (4 hunks)
  • apps/dashboard/app/auth/sign-up/email-signup.tsx (6 hunks)
  • apps/dashboard/app/auth/sign-up/oauth-signup.tsx (1 hunks)
  • apps/dashboard/components/auth/post-auth-invitation-handler.tsx (0 hunks)
  • apps/dashboard/components/auth/turnstile-challenge.tsx (1 hunks)
  • apps/dashboard/lib/auth/base-provider.ts (4 hunks)
  • apps/dashboard/lib/auth/cookies.ts (1 hunks)
  • apps/dashboard/lib/auth/get-auth.ts (1 hunks)
  • apps/dashboard/lib/auth/local.ts (2 hunks)
  • apps/dashboard/lib/auth/sessions.ts (4 hunks)
  • apps/dashboard/lib/auth/types.ts (3 hunks)
  • apps/dashboard/lib/auth/workos.ts (11 hunks)
  • apps/dashboard/lib/env.ts (1 hunks)
  • apps/dashboard/package.json (1 hunks)
  • apps/engineering/content/design/components/search/llm-search.examples.tsx (1 hunks)
  • apps/engineering/content/design/components/toaster.example.tsx (2 hunks)
  • turbo.json (1 hunks)
💤 Files with no reviewable changes (2)
  • apps/dashboard/components/auth/post-auth-invitation-handler.tsx
  • apps/dashboard/app/(app)/api/auth/refresh/route.ts
🧰 Additional context used
🧠 Learnings (12)
📓 Common learnings
Learnt from: Flo4604
Repo: unkeyed/unkey PR: 4190
File: go/internal/services/keys/verifier.go:51-53
Timestamp: 2025-10-30T15:10:52.743Z
Learning: PR #4190 for unkeyed/unkey is focused solely on database schema and query changes for identity-based credits. It adds IdentityCredits and KeyCredits fields to structs and queries, but does not implement the priority enforcement logic in the usagelimiter. The logic implementation is intentionally deferred to a later PR in the stack.
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 4355
File: apps/dashboard/lib/auth/workos.ts:104-144
Timestamp: 2025-11-20T19:10:23.081Z
Learning: In `apps/dashboard/lib/auth/workos.ts`, the `checkRadar()` method intentionally has different default behaviors for different failure modes: (1) When Radar API fails (network error or non-OK HTTP response), it defaults to `{ action: "allow" }` to fail-open and avoid blocking signups when the service is down. (2) When Radar API succeeds (OK response) but `data.verdict` is missing, it defaults to `"block"` via `data.verdict || "block"` to fail-closed as a conservative approach when the service is operational but returns unexpected data. This design prevents both service outages from blocking legitimate users and malformed responses from being treated permissively.
📚 Learning: 2024-10-25T23:53:41.716Z
Learnt from: Srayash
Repo: unkeyed/unkey PR: 2568
File: apps/dashboard/app/auth/sign-up/oauth-signup.tsx:25-25
Timestamp: 2024-10-25T23:53:41.716Z
Learning: In the React component `OAuthSignUp` (`apps/dashboard/app/auth/sign-up/oauth-signup.tsx`), adding a `useEffect` cleanup function to reset the `isLoading` state causes a "something went wrong" popup to appear before redirecting when a user clicks on signup.

Applied to files:

  • apps/dashboard/app/auth/sign-in/email-code.tsx
  • apps/dashboard/app/auth/sign-up/[[...sign-up]]/page.tsx
  • apps/dashboard/app/auth/sign-in/oauth-signin.tsx
  • apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx
  • apps/dashboard/app/auth/sign-up/oauth-signup.tsx
  • apps/dashboard/app/auth/sign-in/email-signin.tsx
  • apps/dashboard/app/auth/hooks/useSignUp.ts
  • apps/dashboard/app/auth/sign-up/email-signup.tsx
  • apps/dashboard/app/auth/sign-up/email-code.tsx
📚 Learning: 2025-11-20T19:10:23.081Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 4355
File: apps/dashboard/lib/auth/workos.ts:104-144
Timestamp: 2025-11-20T19:10:23.081Z
Learning: In `apps/dashboard/lib/auth/workos.ts`, the `checkRadar()` method intentionally has different default behaviors for different failure modes: (1) When Radar API fails (network error or non-OK HTTP response), it defaults to `{ action: "allow" }` to fail-open and avoid blocking signups when the service is down. (2) When Radar API succeeds (OK response) but `data.verdict` is missing, it defaults to `"block"` via `data.verdict || "block"` to fail-closed as a conservative approach when the service is operational but returns unexpected data. This design prevents both service outages from blocking legitimate users and malformed responses from being treated permissively.

Applied to files:

  • apps/dashboard/app/auth/sign-up/[[...sign-up]]/page.tsx
  • apps/dashboard/lib/auth/types.ts
  • apps/dashboard/app/auth/sign-up/oauth-signup.tsx
  • apps/dashboard/app/auth/actions.ts
  • apps/dashboard/lib/auth/local.ts
  • apps/dashboard/lib/auth/workos.ts
  • apps/dashboard/app/auth/hooks/useSignIn.ts
  • apps/dashboard/app/auth/hooks/useSignUp.ts
  • apps/dashboard/lib/auth/base-provider.ts
📚 Learning: 2024-10-23T16:19:42.049Z
Learnt from: p6l-richard
Repo: unkeyed/unkey PR: 2085
File: apps/www/components/glossary/search.tsx:41-57
Timestamp: 2024-10-23T16:19:42.049Z
Learning: For the `FilterableCommand` component in `apps/www/components/glossary/search.tsx`, adding error handling and loading states to the results list is not necessary.

Applied to files:

  • apps/engineering/content/design/components/search/llm-search.examples.tsx
📚 Learning: 2024-10-23T16:21:47.395Z
Learnt from: p6l-richard
Repo: unkeyed/unkey PR: 2085
File: apps/www/components/glossary/search.tsx:16-20
Timestamp: 2024-10-23T16:21:47.395Z
Learning: For the `FilterableCommand` component in `apps/www/components/glossary/search.tsx`, refactoring type definitions into an interface is not necessary at this time.

Applied to files:

  • apps/engineering/content/design/components/search/llm-search.examples.tsx
📚 Learning: 2025-04-30T15:25:33.917Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3210
File: apps/dashboard/app/new/page.tsx:3-3
Timestamp: 2025-04-30T15:25:33.917Z
Learning: There are two different `getAuth` functions in the Unkey codebase with different purposes:
1. `@/lib/auth/get-auth` - Base function without redirects, used in special cases on the dashboard where redirect control is needed (like `/new` page) and within tRPC context
2. `@/lib/auth` - Helper function with redirects, used in most dashboard cases (approximately 98%)

Applied to files:

  • apps/dashboard/lib/auth/get-auth.ts
📚 Learning: 2025-05-05T17:55:59.607Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 3215
File: apps/dashboard/lib/auth/sessions.ts:47-51
Timestamp: 2025-05-05T17:55:59.607Z
Learning: Local auth cookies in apps/dashboard/lib/auth/sessions.ts intentionally omit HttpOnly and Secure flags to allow easier debugging during local development. This is by design as these cookies are only used in local development environments, not production.

Applied to files:

  • apps/dashboard/lib/auth/sessions.ts
  • apps/dashboard/lib/auth/local.ts
  • apps/dashboard/lib/auth/cookies.ts
📚 Learning: 2025-06-02T11:09:58.791Z
Learnt from: ogzhanolguncu
Repo: unkeyed/unkey PR: 3292
File: apps/dashboard/lib/trpc/routers/key/create.ts:11-14
Timestamp: 2025-06-02T11:09:58.791Z
Learning: In the unkey codebase, TypeScript and the env() function implementation already provide sufficient validation for environment variables, so additional runtime error handling for missing env vars is not needed.

Applied to files:

  • apps/dashboard/app/auth/actions.ts
  • apps/dashboard/lib/env.ts
📚 Learning: 2025-09-24T18:57:34.843Z
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 4010
File: QUICKSTART-DEPLOY.md:17-17
Timestamp: 2025-09-24T18:57:34.843Z
Learning: In the Unkey deployment platform, API key environment variables use component-specific naming but share the same secret value: UNKEY_API_KEY for the ctrl service (validator), API_KEY for the CLI client, and CTRL_API_KEY for the dashboard client. The ctrl service acts as the source of truth for validation.

Applied to files:

  • apps/dashboard/app/auth/actions.ts
📚 Learning: 2025-08-08T16:07:48.307Z
Learnt from: imeyer
Repo: unkeyed/unkey PR: 3755
File: .github/actions/setup-node/action.yaml:37-40
Timestamp: 2025-08-08T16:07:48.307Z
Learning: Repo unkeyed/unkey — pnpm immutable installs are enforced by setting the CI environment variable; any truthy value (e.g., "1" or "true") is acceptable. Do not require the literal string "true". Applies to .github/actions/setup-node/action.yaml and all workflows using pnpm install.

Applied to files:

  • apps/dashboard/app/auth/actions.ts
📚 Learning: 2024-10-23T12:05:31.121Z
Learnt from: chronark
Repo: unkeyed/unkey PR: 2544
File: apps/api/src/pkg/env.ts:4-6
Timestamp: 2024-10-23T12:05:31.121Z
Learning: The `cloudflareRatelimiter` type definition in `apps/api/src/pkg/env.ts` should not have its interface changed; it should keep the `limit` method returning `Promise<{ success: boolean }>` without additional error properties.

Applied to files:

  • apps/dashboard/app/auth/actions.ts
📚 Learning: 2025-09-01T16:43:57.850Z
Learnt from: perkinsjr
Repo: unkeyed/unkey PR: 3898
File: apps/dashboard/app/(app)/settings/general/page.tsx:15-18
Timestamp: 2025-09-01T16:43:57.850Z
Learning: In the Unkey dashboard, orgId guards are intentionally duplicated across pages rather than extracted to helpers because each page needs different additional context (user details, DB connections, subscriptions, etc.). The orgId check serves as both authentication and handling edge cases where users sign up but don't have organization/workspace setup completed.

Applied to files:

  • apps/dashboard/app/auth/hooks/useSignIn.ts
🧬 Code graph analysis (8)
apps/dashboard/lib/auth/sessions.ts (2)
apps/dashboard/lib/auth/types.ts (1)
  • UNKEY_SESSION_COOKIE (4-4)
apps/dashboard/lib/auth/cookies.ts (1)
  • getCookieOptionsAsString (142-178)
apps/dashboard/app/auth/sign-in/email-signin.tsx (3)
apps/dashboard/app/auth/hooks/useSignIn.ts (1)
  • useSignIn (46-231)
apps/dashboard/lib/auth/types.ts (1)
  • PendingTurnstileResponse (94-103)
apps/dashboard/components/auth/turnstile-challenge.tsx (1)
  • TurnstileChallenge (13-71)
apps/dashboard/app/auth/actions.ts (2)
apps/dashboard/lib/env.ts (1)
  • env (3-55)
apps/dashboard/lib/auth/types.ts (2)
  • PendingTurnstileResponse (94-103)
  • EmailAuthResult (106-106)
apps/dashboard/lib/auth/local.ts (1)
apps/dashboard/lib/auth/types.ts (1)
  • UserData (196-200)
apps/dashboard/lib/auth/workos.ts (1)
apps/dashboard/lib/auth/types.ts (2)
  • UserData (196-200)
  • EmailAuthResult (106-106)
apps/dashboard/app/auth/hooks/useSignIn.ts (1)
apps/dashboard/lib/auth/types.ts (3)
  • EmailAuthResult (106-106)
  • PendingTurnstileResponse (94-103)
  • errorMessages (219-240)
apps/dashboard/app/auth/hooks/useSignUp.ts (3)
apps/dashboard/lib/auth/types.ts (4)
  • EmailAuthResult (106-106)
  • PendingTurnstileResponse (94-103)
  • UserData (196-200)
  • VerificationResult (107-110)
apps/dashboard/app/auth/actions.ts (5)
  • signUpViaEmail (76-79)
  • verifyTurnstileAndRetry (444-489)
  • verifyAuthCode (86-214)
  • verifyEmail (216-244)
  • resendAuthCode (246-285)
apps/dashboard/lib/auth/workos.ts (4)
  • signUpViaEmail (578-647)
  • verifyAuthCode (714-770)
  • verifyEmail (772-827)
  • resendAuthCode (705-712)
apps/dashboard/lib/auth/base-provider.ts (1)
apps/dashboard/lib/auth/types.ts (2)
  • VerificationResult (107-110)
  • UserData (196-200)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (4)
  • GitHub Check: Test Packages / Test
  • GitHub Check: Test Dashboard / Test Dashboard
  • GitHub Check: autofix
  • GitHub Check: Analyze (javascript-typescript)
🔇 Additional comments (38)
apps/engineering/content/design/components/search/llm-search.examples.tsx (1)

117-117: LGTM! Console logging removed as intended.

The change from console logging to a no-op callback aligns with the PR objectives to remove console logs across the codebase. For this example component that focuses on demonstrating placeholder customization, the no-op callback is appropriate—the loading state behavior is still demonstrated through the useSearchState hook, and the underscore prefix correctly indicates the intentionally unused parameter.

apps/engineering/content/design/components/toaster.example.tsx (1)

155-155: Inconsistency between code snippet and implementation.

The customCodeSnippet (lines 116, 129, 133) still displays console.log statements, but the actual rendered component now uses empty functions. This mismatch will confuse developers who copy the snippet expecting console output.

For documentation examples, console.log is often appropriate to demonstrate that callbacks execute. Consider either:

  1. Reverting to console.log in both the snippet and implementation for consistency and educational value
  2. Updating the customCodeSnippet to match the empty function implementation

Apply this diff to align the snippet with the implementation:

           action: {
             label: "Undo",
-            onClick: () => console.log("Undo clicked"),
+            onClick: () => {},
           },
           action: {
             label: "Delete",
-            onClick: () => console.log("Delete confirmed"),
+            onClick: () => {},
           },
           cancel: {
             label: "Cancel",
-            onClick: () => console.log("Delete cancelled"),
+            onClick: () => {},
           },

Also applies to: 168-168, 172-172

⛔ Skipped due to learnings
Learnt from: MichaelUnkey
Repo: unkeyed/unkey PR: 3425
File: apps/engineering/content/design/components/filter/control-cloud.examples.tsx:73-83
Timestamp: 2025-07-02T14:13:01.711Z
Learning: In apps/engineering/content/design/components/, when the `RenderComponentWithSnippet` component does not render code snippets correctly, use the `customCodeSnippet` prop to manually provide the correct JSX code as a string. This manual approach is necessary due to technical limitations in the automatic rendering mechanism.
apps/dashboard/app/auth/sign-in/oauth-signin.tsx (1)

38-42: Confirm error tracking strategy for OAuth failures.

Verification found no structured error tracking infrastructure (Sentry, Datadog, logging service, etc.) in the codebase. OAuth failures will be silently caught with only a generic toast message and no observability for debugging or monitoring.

While the removal of console.error() in client-side code is reasonable, losing all visibility into OAuth failures is problematic for a critical authentication flow. Before merging:

  • Confirm your team has an alternative error tracking/monitoring strategy for auth flows
  • If not, consider either adding structured error tracking (e.g., Sentry) or restoring error logging with additional context for the error handling service
  • This change appears to be part of systematic logging cleanup; consider separating it into a dedicated PR for better change isolation and review focus
apps/dashboard/lib/auth/cookies.ts (1)

124-126: Expanded setLastUsedOrgCookie param object looks good

This is a pure signature/formatting tweak; call shape and behavior are unchanged, and the explicit object type improves readability without side effects.

apps/dashboard/lib/auth/sessions.ts (2)

49-51: Local auth Set-Cookie fallback remains correct

Only the string interpolation is reflowed; the emitted cookie (Path=/; SameSite=Strict; Max-Age=10y) is unchanged and still matches the intentional choice to omit HttpOnly/Secure for local-only auth cookies. Based on learnings, this continues to align with the local-debug design.


114-118: Refreshed-session Set-Cookie construction stays consistent with defaults

Using getCookieOptionsAsString({ expiresAt: refreshedSession.expiresAt }) inside the template keeps the refreshed session cookie aligned with your centralized defaults (path, SameSite, HttpOnly/Secure, etc.) in both the primary and fallback paths; the changes here are formatting-only and maintain correct behavior.

Also applies to: 132-136

apps/dashboard/app/auth/sign-up/oauth-signup.tsx (1)

20-37: Catch block cleanup aligns with existing UX

Renaming the error param to _err and relying solely on the toast keeps the user-facing behavior while avoiding noisy console logs and unused-variable warnings. No issues from a flow or correctness perspective.

apps/dashboard/app/api/auth/invitation/route.ts (1)

51-55: Generic 500 response without logging is fine here

Catching as _error and returning a generic 500 with no-store keeps the API contract intact and avoids leaking internals, which is consistent with the rest of the auth routes in this PR.

apps/dashboard/app/api/auth/accept-invitation/route.ts (1)

41-46: Error-handling updates are consistent with route behavior

Switching to _error and dropping console logging in both catches keeps the existing response semantics (400 for bad tokens, 500 for unexpected failures) while matching the quieter logging pattern used elsewhere in this PR.

Also applies to: 104-106

apps/dashboard/lib/auth/get-auth.ts (1)

14-34: Graceful fallback to unauthenticated state remains intact

Catching as _error and returning null userId/orgId/role on failure preserves the existing behavior of treating errors as “no auth” for this non-redirecting helper, which is appropriate for its usage.

Based on learnings, this aligns with the intended role of @/lib/auth/get-auth as the base, non-redirecting auth helper.

apps/dashboard/package.json (1)

103-104: @marsidev/react-turnstile@^1.0.2 is compatible with React 18 and Next.js 14

The package declares peerDependencies supporting React 18 and React DOM 18, and version 1.3.1 is the latest release. The package includes SSR/Next.js support, so ^1.0.2 is well-suited for your setup. Ensure all usages stay in "use client" components and that the Turnstile site key env is properly configured.

apps/dashboard/app/auth/sign-up/[[...sign-up]]/page.tsx (1)

41-42: LGTM! Accurate error message.

The error message now correctly reflects that this is a sign-up failure, not a sign-in failure, improving debugging clarity.

apps/dashboard/app/auth/sign-in/[[...sign-in]]/page.tsx (2)

53-55: Silent error suppression acceptable here.

Ignoring cookie read errors is reasonable since the application continues to function normally without the last-used org preference.


125-127: Silent error suppression acceptable for auto sign-in.

Since this is an automatic convenience feature and users can manually sign in if it fails, silently ignoring errors here is acceptable. The loading state is properly reset in the finally block.

apps/dashboard/lib/env.ts (1)

52-53: LGTM! Proper separation of public and secret keys.

The environment variables follow Next.js conventions correctly:

  • NEXT_PUBLIC_CLOUDFLARE_TURNSTILE_SITE_KEY is client-accessible for the Turnstile widget
  • CLOUDFLARE_TURNSTILE_SECRET_KEY remains server-side only for verification

Both are appropriately marked optional to support environments where Turnstile is not configured.

apps/dashboard/app/auth/sign-up/email-code.tsx (3)

102-111: Excellent form handling improvements.

The conversion to a proper <form> element with submit handling and loading guards prevents race conditions and improves accessibility. The form can now be submitted via Enter key, and concurrent submissions are properly prevented.


116-122: Good race condition prevention.

Adding the isLoading check in onComplete and the disabled prop prevents multiple concurrent verification attempts, improving UX and reducing unnecessary API calls.


155-155: Fixed class name formatting issue.

Removing the trailing space in the conditional class ensures proper CSS class application when the slot is active.

apps/dashboard/lib/auth/types.ts (3)

93-103: Verify optional fields in challengeParams.

The challengeParams structure has ipAddress and userAgent as optional, while authMethod and action are required. Ensure this is intentional and that the verification flow can handle missing IP address or user agent information gracefully.

If these fields are always expected to be present during Radar challenges, consider making them required to catch configuration issues earlier.

Please verify:

  1. Are there legitimate scenarios where ipAddress or userAgent might be unavailable during a Radar challenge?
  2. Does the Turnstile verification flow handle missing optional fields correctly?

106-106: LGTM! Consistent type union expansion.

Adding PendingTurnstileResponse to the EmailAuthResult union follows the established pattern for handling special authentication states like org selection and email verification.


216-216: LGTM! Clear error messaging for Turnstile challenges.

The new error code and user-facing message clearly communicate that a verification challenge is required, maintaining consistency with other auth error messages in the codebase.

Also applies to: 238-239

apps/dashboard/lib/auth/local.ts (1)

367-376: LGTM! API compatibility maintained.

Adding the bypassRadar parameter to both methods maintains API compatibility with the WorkOS provider. It's appropriate that the local development provider ignores this parameter since it's a no-op implementation that always succeeds.

Also applies to: 378-386

turbo.json (1)

41-42: No issues found with removal of Clerk environment variables.

Verification confirms that CLERK_WEBHOOK_SECRET and CLERK_SECRET_KEY are defined in the Zod schema (apps/dashboard/lib/env.ts:19-20) as optional fields but are never actually used in the codebase. Their removal from turbo.json is safe and will not break any functionality.

apps/dashboard/app/auth/actions.ts (3)

246-285: Ratelimit onError fail-open behavior looks intentional and consistent

Falling back to { success: true, ... } from onError keeps resend flow available when the ratelimit backend fails, which matches a fail-open design for non-security-critical rate limiting. The _err rename also avoids unused-variable noise without changing semantics.

No change needed here.

Also applies to: 257-265


441-489: Turnstile retry flow and bypassRadar usage look correct; confirm action values are stable

The verifyTurnstileAndRetry helper correctly:

  • Verifies the Turnstile token server-side.
  • On success, retries the original operation with bypassRadar: true so Radar isn’t re-run after a human challenge.
  • Distinguishes between "sign-up" (with userData) vs "sign-in" actions.

Two follow-ups to consider:

  • Ensure the challengeParams.action values ("sign-up" / "sign-in") are treated as constants throughout the stack (ideally centralised as string literals or an enum) to avoid typos.
  • If you ever add more actions, this function will currently fall back to UNKNOWN_ERROR; documenting/typing allowed action values in PendingTurnstileResponse["challengeParams"] would help catch that at compile time.

Overall flow looks good.


42-73: Turnstile verification likely broken due to JSON payload instead of form-encoded body

Cloudflare Turnstile’s siteverify endpoint expects secret and response as URL-encoded form data; sending JSON here risks every verification failing and permanently blocking challenged logins/signups.

Consider switching to application/x-www-form-urlencoded with URLSearchParams:

-  try {
-    const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
-      method: "POST",
-      headers: {
-        "Content-Type": "application/json",
-      },
-      body: JSON.stringify({
-        secret: secretKey,
-        response: token,
-      }),
-    });
+  try {
+    const response = await fetch("https://challenges.cloudflare.com/turnstile/v0/siteverify", {
+      method: "POST",
+      headers: {
+        "Content-Type": "application/x-www-form-urlencoded",
+      },
+      body: new URLSearchParams({
+        secret: secretKey,
+        response: token,
+      }),
+    });

This keeps the rest of the logic (fail-closed on missing key / non-OK / exceptions) intact.

⛔ Skipped due to learnings
Learnt from: mcstepp
Repo: unkeyed/unkey PR: 4355
File: apps/dashboard/lib/auth/workos.ts:104-144
Timestamp: 2025-11-20T19:10:23.081Z
Learning: In `apps/dashboard/lib/auth/workos.ts`, the `checkRadar()` method intentionally has different default behaviors for different failure modes: (1) When Radar API fails (network error or non-OK HTTP response), it defaults to `{ action: "allow" }` to fail-open and avoid blocking signups when the service is down. (2) When Radar API succeeds (OK response) but `data.verdict` is missing, it defaults to `"block"` via `data.verdict || "block"` to fail-closed as a conservative approach when the service is operational but returns unexpected data. This design prevents both service outages from blocking legitimate users and malformed responses from being treated permissively.
apps/dashboard/app/auth/hooks/useSignIn.ts (2)

37-44: Turnstile challenge type guard is precise and safe

isPendingTurnstileChallenge checks success === false, the specific RADAR_CHALLENGE_REQUIRED code, and required email/challengeParams fields. This safely narrows EmailAuthResult to PendingTurnstileResponse without overlapping normal error responses.

Nice, clear guard.


185-218: Turnstile verification handler behavior is consistent with other auth flows

handleTurnstileVerification:

  • Clears existing errors, calls verifyTurnstileAndRetry, and sets setIsVerifying(true) on success.
  • Reuses the same error handling as handleSignInViaEmail, including ACCOUNT_NOT_FOUNDsetAccountNotFound(true) and generic fallback to UNKNOWN_ERROR.
  • Propagates unexpected exceptions so the caller can handle them.

This keeps Turnstile sign-in behavior aligned with the non-challenge path and centralizes the Radar/Turnstile specifics in the server actions.

Looks good to me.

apps/dashboard/app/auth/sign-up/email-signup.tsx (3)

36-43: Form validation and disabled submit behavior are solid and consistent

  • isValidEmail + isFormValid gate the submit button, preventing obviously bad input.
  • missingFields + validationError give clear, accessible feedback (role="alert", aria-live="polite").
  • Button disabled={isLoading || !isFormValid} matches this and avoids double submissions.

This is a nice improvement over relying solely on server-side validation.

Also applies to: 65-71, 213-216


80-103: Sign-up Turnstile challenge handling looks correct

The sign-up flow:

  • Calls handleSignUpViaEmail(...).
  • If isPendingTurnstileChallenge(result), stores the challenge and flips off isLoading.
  • If result?.success, directly advances to verification.

This cleanly separates the Turnstile step while keeping existing behavior for non-challenged users. The catch block correctly maps thrown errors to AuthErrorCode when possible and falls back to UNKNOWN_ERROR.

No change needed here.


126-147: Turnstile error and retry UX are clear

When a Turnstile error occurs:

  • You clear the challenge and show a toast (“Verification failed. Please try again.”).
  • You present a “Try different information” button to restart with a fresh form.

This gives users a straightforward escape hatch from a failed challenge without leaving them stuck.

Looks good.

apps/dashboard/lib/auth/base-provider.ts (2)

56-62: bypassRadar wiring in email flows is appropriate—ensure all providers implement it

Adding optional bypassRadar?: boolean to signInViaEmail and signUpViaEmail matches the Turnstile retry flow, where you only set it after a successful human verification.

Please double-check that all concrete providers (workos, local, etc.):

  • Update their method signatures to match this abstract class.
  • Respect bypassRadar only in the intended contexts (i.e., skip Radar checks when it’s true, not by default).

This will keep TS happy and ensure consistent behavior across providers.

Also applies to: 100-106


282-310: Removing console.error from handleError is fine; response semantics unchanged

handleError still:

  • Maps known AuthErrorCode messages to the corresponding errorMessages entry.
  • Falls back to an UNKNOWN_ERROR with either the original error.message or the generic message.

Dropping console.error keeps logs cleaner and avoids noisy server output, especially for expected auth failures. As long as provider implementations log unexpected errors where appropriate, this is a good simplification.

apps/dashboard/app/auth/sign-in/email-signin.tsx (2)

14-31: Email validation, form handling, and Turnstile challenge branching look good

  • currentEmail + isValidEmailisFormValid cleanly control the disabled state of the submit button.
  • handleSubmit still uses FormData for the actual email value, which is fine and consistent.
  • When a Turnstile challenge is returned, you store it, drop the loading spinner, and early-return, so the user clearly transitions into the challenge step.

This is a solid integration that doesn’t disturb the existing LastUsed behavior.

Also applies to: 32-56


100-122: Disabled button styling and loading state behavior remain consistent

  • The new button class adds disabled styling (opacity-50, disabled:cursor-not-allowed, etc.), which matches how sign-up behaves.
  • disabled={isLoading || !isFormValid} ensures we don’t send extra requests and don’t even try with obviously invalid emails.
  • The clientReady && isLoading check retains the pre-hydration safeguard for showing the spinner.

No issues here.

apps/dashboard/app/auth/hooks/useSignUp.ts (2)

22-31: Type guard and returned API surface for Turnstile challenges look consistent

isPendingTurnstileChallenge correctly narrows EmailAuthResult to PendingTurnstileResponse by checking success, code === RADAR_CHALLENGE_REQUIRED, and the presence of email and challengeParams. Exposing it (plus handleTurnstileVerification) from the hook gives the UI a clean way to branch on the Turnstile path, and the return type of EmailAuthResult matches the union defined in @/lib/auth/types.

Also applies to: 102-112


65-79: Code and email verification handlers match server expectations and validate inputs

handleCodeVerification’s check that userData.email exists before calling verifyAuthCode({ email, code, invitationToken }) aligns with the WorkOS provider’s verifyAuthCode signature (apps/dashboard/app/auth/workos.ts:713-769). handleEmailVerification simply delegating to verifyEmail(code) keeps the client-side surface minimal while letting the server action pull the pending token from cookies as needed. No issues here.

Also applies to: 81-83

apps/dashboard/lib/auth/workos.ts (1)

104-141: Radar + Turnstile integration and bypassRadar flow are coherent and match prior design

checkRadar still:

  • Returns { action: "allow" } on non‑OK HTTP responses or exceptions, and
  • Defaults to "block" when data.verdict is falsy via data.verdict || "block",
    which preserves the fail‑open vs fail‑closed behavior described in the earlier learning for Radar. Returning a Decision object with reason attached is a nice improvement for observability. Based on learnings, this keeps the intended safety profile intact.

In signUpViaEmail and signInViaEmail:

  • bypassRadar?: boolean is only honored to skip Radar once the client has gone through the Turnstile flow; initial calls hit Radar as before.
  • On radarDecision.action === "challenge", both methods now return a structured RADAR_CHALLENGE_REQUIRED response with email and challengeParams { ipAddress, userAgent, authMethod, action }, matching PendingTurnstileResponse (apps/dashboard/lib/auth/types.ts:93-102).
  • On radarDecision.action === "block", they still hard‑fail with AuthErrorCode.RADAR_BLOCKED, keeping blocked requests distinct from challenge flows.
  • After Turnstile verification, verifyTurnstileAndRetry calls back into signUpViaEmail/signInViaEmail with bypassRadar: true (apps/dashboard/app/auth/actions.ts:443-488), so we don’t loop on Radar.

Overall this wiring correctly gates OTP sending on either Radar “allow” or a successful Turnstile challenge.

Also applies to: 578-647, 649-689

Copy link
Collaborator

@MichaelUnkey MichaelUnkey left a comment

Choose a reason for hiding this comment

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

LGTM

Copy link
Collaborator

@mcstepp mcstepp left a comment

Choose a reason for hiding this comment

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

fine, remove all the error logging, idc 😭

looks good, i tested sign-up with VPN and playwright to trigger some challenges.

@graphite-app
Copy link

graphite-app bot commented Nov 24, 2025

Not available (Added via Giphy)

@graphite-app
Copy link

graphite-app bot commented Nov 24, 2025

Graphite Automations

"Post a GIF when PR approved" took an action on this PR • (11/24/25)

1 gif was posted to this PR based on Andreas Thomas's automation.

@perkinsjr perkinsjr merged commit c6ed44c into main Nov 24, 2025
22 checks passed
@perkinsjr perkinsjr deleted the captcha-on-login branch November 24, 2025 19:07
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