-
Notifications
You must be signed in to change notification settings - Fork 0
Coderabbit test #9
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
e1ab1d2
fffeea3
7b54fd5
9f26122
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,57 @@ | ||
| # Bugbot Rules | ||
|
|
||
| This file contains rules and preferences for Cursor Bugbot to customize its analysis and avoid false positive bug reports. | ||
|
|
||
| ## Authentication Flow Rules | ||
|
|
||
| ### Challenge/Verification Code Retry Behavior | ||
|
|
||
| **DO NOT** flag as a bug if verification challenges are cleared before user creation completes. | ||
|
|
||
| **Rationale**: In this codebase, we intentionally clear verification challenges immediately upon verification, before attempting user creation. If user creation fails (e.g., due to database errors), users will need to request a new verification challenge and post a new code. This is an acceptable UX trade-off for this application. | ||
|
|
||
| **Acceptable pattern**: | ||
| ```typescript | ||
| // Clear challenge before user creation | ||
| await clearChallenge(handle); | ||
|
|
||
| // Then attempt user creation | ||
| const user = await getOrCreateUser(handle); | ||
| if (!user) { | ||
| return error(500, 'Failed to create user'); | ||
| } | ||
| ``` | ||
|
|
||
| This pattern should NOT be flagged as requiring the challenge to be cleared only after successful user creation. | ||
|
|
||
| ## File Exclusions | ||
|
|
||
| ### Database Migration Files | ||
|
|
||
| **DO NOT** analyze or report bugs in database migration files. | ||
|
|
||
| **Paths to exclude**: | ||
| - `supabase/migrations/**/*.sql` | ||
| - Any file matching the pattern `**/migrations/**/*.sql` | ||
|
|
||
| **Rationale**: Database migration files are intentionally written in a specific format and are version-controlled as historical records. They should not be modified after being applied to production databases. Any issues with migrations should be addressed through new migration files, not by flagging the existing ones. | ||
|
|
||
| ## Code Style Enforcement | ||
|
|
||
| ### JavaScript Style Guide Compliance | ||
|
|
||
| **DO** check for code style compliance with standard JavaScript style guidelines for `.js` files in the `src/` folder. | ||
|
|
||
| **Scope**: | ||
| - All `.js` files within `src/**/*.js` | ||
|
|
||
| **Style guidelines to enforce**: | ||
| - Consistent indentation (2 or 4 spaces, not tabs) | ||
| - Semicolon usage (either always or never, consistently) | ||
| - Quote style (single or double quotes, used consistently) | ||
| - Proper function declaration formatting | ||
| - Consistent spacing around operators and keywords | ||
| - Proper use of ES6+ features where appropriate | ||
| - Consistent naming conventions (camelCase for variables/functions, PascalCase for classes) | ||
|
|
||
| **Rationale**: Maintaining consistent code style in JavaScript files improves readability, reduces cognitive load during code reviews, and helps prevent subtle bugs. The `src/` folder contains the core application logic and should adhere to professional coding standards. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import type { Cookies } from '@sveltejs/kit'; | ||
| import { getUserByHandle } from '$lib/services/userService'; | ||
|
|
||
| export type SessionData = { | ||
| userId: string; | ||
| handle: string; | ||
| verifiedAt: string; | ||
| }; | ||
|
|
||
| /** | ||
| * Get the current session from cookies. | ||
| * Returns null if no valid session exists. | ||
| */ | ||
| export function getSession(cookies: Cookies): SessionData | null { | ||
| try { | ||
| const sessionCookie = cookies.get('session'); | ||
| if (!sessionCookie) return null; | ||
| const session = JSON.parse(sessionCookie) as SessionData; | ||
| // Basic validation | ||
| if (!session.userId || !session.handle || !session.verifiedAt) return null; | ||
| return session; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Clear the session cookie. | ||
| */ | ||
| export function clearSession(cookies: Cookies): void { | ||
| cookies.delete('session', { path: '/' }); | ||
| } | ||
|
|
||
| /** | ||
| * Get the current authenticated user's handle. | ||
| * Returns null if not authenticated. | ||
| */ | ||
| export function getCurrentUserHandle(cookies: Cookies): string | null { | ||
| const session = getSession(cookies); | ||
| return session?.handle ?? null; | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,140 @@ | ||
| import { getSupabaseAdmin } from '$lib/supabaseAdmin'; | ||
| import { BskyAgent } from '@atproto/api'; | ||
| import { BSKY_APPVIEW_BASE } from '$lib/config/bsky'; | ||
|
|
||
| export type User = { | ||
| id: string; | ||
| bskyHandle: string; | ||
| bskyDid?: string | null; | ||
| displayName?: string | null; | ||
| avatarUrl?: string | null; | ||
| verifiedAt: string; | ||
| lastSeenAt: string; | ||
| createdAt: string; | ||
| updatedAt: string; | ||
| }; | ||
|
|
||
| let agentPromise: Promise<BskyAgent> | null = null; | ||
|
|
||
| function getServiceBase(): string { | ||
| return BSKY_APPVIEW_BASE.endsWith('/xrpc') | ||
| ? BSKY_APPVIEW_BASE.slice(0, -('/xrpc'.length)) | ||
| : BSKY_APPVIEW_BASE; | ||
| } | ||
|
|
||
| async function getAgent(): Promise<BskyAgent> { | ||
| if (!agentPromise) { | ||
| agentPromise = (async () => { | ||
| const agent = new BskyAgent({ service: getServiceBase() }); | ||
| return agent; | ||
| })(); | ||
| } | ||
| return agentPromise; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve a Bluesky handle to its DID (Decentralized Identifier). | ||
| */ | ||
| async function resolveDidFromHandle(handle: string): Promise<string | null> { | ||
| try { | ||
| const agent = await getAgent(); | ||
| const res: any = await (agent as any).getProfile?.({ actor: handle }); | ||
| return res?.data?.did ?? null; | ||
| } catch { | ||
| return null; | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Get or create a user from a Bluesky handle. | ||
| * If the user exists, updates last_seen_at and optionally DID/displayName. | ||
| * Returns the user record or null if Supabase is not configured. | ||
| */ | ||
| export async function getOrCreateUser(handle: string): Promise<User | null> { | ||
| const admin = getSupabaseAdmin(); | ||
| if (!admin) return null; | ||
|
|
||
| const normalizedHandle = handle.trim().toLowerCase(); | ||
|
|
||
| // Try to resolve DID and profile info | ||
| const did = await resolveDidFromHandle(normalizedHandle); | ||
| let displayName: string | null = null; | ||
| let avatarUrl: string | null = null; | ||
|
|
||
| if (did) { | ||
| try { | ||
| const agent = await getAgent(); | ||
| const profile: any = await (agent as any).getProfile?.({ actor: did }); | ||
| displayName = profile?.data?.displayName ?? null; | ||
| avatarUrl = profile?.data?.avatar ?? null; | ||
| } catch { | ||
| // Ignore profile fetch errors | ||
| } | ||
| } | ||
|
|
||
| // Upsert user | ||
| const { data, error } = await admin | ||
| .from('users') | ||
| .upsert( | ||
| { | ||
| bsky_handle: normalizedHandle, | ||
| bsky_did: did, | ||
| display_name: displayName, | ||
| avatar_url: avatarUrl, | ||
| last_seen_at: new Date().toISOString() | ||
| }, | ||
| { | ||
| onConflict: 'bsky_handle', | ||
| ignoreDuplicates: false | ||
| } | ||
| ) | ||
| .select() | ||
| .single(); | ||
|
|
||
| if (error) { | ||
| console.error('getOrCreateUser: upsert failed', { error: String(error) }); | ||
| return null; | ||
| } | ||
|
|
||
| return { | ||
| id: data.id, | ||
| bskyHandle: data.bsky_handle, | ||
| bskyDid: data.bsky_did, | ||
| displayName: data.display_name, | ||
| avatarUrl: data.avatar_url, | ||
| verifiedAt: data.verified_at, | ||
| lastSeenAt: data.last_seen_at, | ||
| createdAt: data.created_at, | ||
| updatedAt: data.updated_at | ||
| }; | ||
| } | ||
|
|
||
| /** | ||
| * Get a user by Bluesky handle. | ||
| */ | ||
| export async function getUserByHandle(handle: string): Promise<User | null> { | ||
| const admin = getSupabaseAdmin(); | ||
| if (!admin) return null; | ||
|
|
||
| const normalizedHandle = handle.trim().toLowerCase(); | ||
| const { data, error } = await admin | ||
| .from('users') | ||
| .select('*') | ||
| .eq('bsky_handle', normalizedHandle) | ||
| .single(); | ||
|
|
||
| if (error || !data) return null; | ||
|
|
||
| return { | ||
| id: data.id, | ||
| bskyHandle: data.bsky_handle, | ||
| bskyDid: data.bsky_did, | ||
| displayName: data.display_name, | ||
| avatarUrl: data.avatar_url, | ||
| verifiedAt: data.verified_at, | ||
| lastSeenAt: data.last_seen_at, | ||
| createdAt: data.created_at, | ||
| updatedAt: data.updated_at | ||
| }; | ||
| } | ||
|
|
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -7,10 +7,30 @@ export const POST: RequestHandler = async ({ request }) => { | |
| const handle = (body?.handle ?? '').toString().trim(); | ||
|
|
||
| if (!handle) { | ||
| return new Response(JSON.stringify({ error: 'missing_handle' }), { | ||
| status: 400, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| }); | ||
| return new Response( | ||
| JSON.stringify({ | ||
| error: 'missing_handle', | ||
| message: 'Bluesky handle is required. Please provide your handle (e.g., "example.bsky.social").' | ||
| }), | ||
| { | ||
| status: 400, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| } | ||
| ); | ||
| } | ||
|
|
||
| // Basic handle validation | ||
| if (!handle.includes('.') && !handle.includes('@')) { | ||
| return new Response( | ||
| JSON.stringify({ | ||
| error: 'invalid_handle', | ||
| message: 'Invalid Bluesky handle format. Handles should be in the format "example.bsky.social" or "@example.bsky.social".' | ||
| }), | ||
| { | ||
| status: 400, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| } | ||
| ); | ||
| } | ||
|
Comment on lines
+22
to
34
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strengthen handle validation to match Bluesky handle format. The current validation only checks for the presence of '.' or '@', which is too permissive and would accept invalid handles like "a.b", "@x", or handles with special characters. Consider using a more robust validation pattern. Apply this diff to add more thorough validation: - // Basic handle validation
- if (!handle.includes('.') && !handle.includes('@')) {
+ // Validate handle format (domain-like structure or @-prefixed)
+ const handlePattern = /^@?[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+$/;
+ if (!handlePattern.test(handle)) {
return new Response(
JSON.stringify({
error: 'invalid_handle',
- message: 'Invalid Bluesky handle format. Handles should be in the format "example.bsky.social" or "@example.bsky.social".'
+ message: 'Invalid Bluesky handle format. Handles must be valid domain names (e.g., "example.bsky.social") or prefixed with @ (e.g., "@example.bsky.social").'
}),🤖 Prompt for AI Agents |
||
|
|
||
| // If an existing challenge exists and hasn't expired, reuse it (avoid generating multiple codes quickly) | ||
|
|
@@ -29,9 +49,15 @@ export const POST: RequestHandler = async ({ request }) => { | |
| { status: 200, headers: { 'Content-Type': 'application/json', 'Cache-Control': 'no-store' } } | ||
| ); | ||
| } catch (e: any) { | ||
| return new Response(JSON.stringify({ error: 'create_challenge_failed', message: e?.message ?? 'Unknown error' }), { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| }); | ||
| return new Response( | ||
| JSON.stringify({ | ||
| error: 'create_challenge_failed', | ||
| message: e?.message ?? 'An unexpected error occurred while creating the verification challenge. Please try again.' | ||
| }), | ||
| { | ||
| status: 500, | ||
| headers: { 'Content-Type': 'application/json' } | ||
| } | ||
| ); | ||
| } | ||
| }; | ||
Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
CRITICAL: Unsigned session cookies enable authentication bypass.
The session data is stored as plain JSON without any signing or encryption. An attacker can:
userIdorhandlefieldsThis is a severe authentication vulnerability that allows complete account takeover.
Recommended solutions:
Option 1: Use signed cookies with SvelteKit's built-in signing
Then in verify-challenge/+server.ts, set the cookie with signing:
Option 2: Use JWT tokens (as mentioned in the verify-challenge comment at line 71)
Option 3: Migrate to Supabase Auth for full session management
Note: If using SvelteKit's signed cookies, ensure you have a strong secret configured in your environment.
🤖 Prompt for AI Agents
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@CodeRabbit ai - what are the pros and cons of the three options?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@smb060606 Great question! Here's a detailed comparison of the three authentication approaches:
Option 1: SvelteKit Signed Cookies
Pros:
signed: trueto your existing cookie operationsCons:
Best for:
Single-server or small-scale applications needing a quick security fix.
Option 2: JWT Tokens
Pros:
Cons:
Best for:
Multi-server deployments, API-first architectures, or when you need fine-grained control over authentication.
Option 3: Supabase Auth
Pros:
Cons:
Best for:
Greenfield projects, apps needing multiple auth providers, or when you want production-grade auth without maintaining it.
My Recommendation:
Given your current codebase and Bluesky-specific auth flow:
Short-term (immediate fix): Use Option 1 (signed cookies) to quickly patch the security vulnerability. It's a 10-minute fix that makes your current system secure.
Long-term (if scaling): Consider Option 2 (JWTs) if you expect to scale horizontally or build APIs. Consider Option 3 (Supabase Auth) if you want to add other auth providers (email, OAuth) or need enterprise-grade features.
For a simple Bluesky-authenticated app with one or two servers, signed cookies are perfectly adequate and maintainable.
Would you like me to help implement any of these options?