Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions bugbot.md
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.
42 changes: 42 additions & 0 deletions src/lib/auth/session.ts
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';

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

Unused import getUserByHandle.

Suggested change
import { getUserByHandle } from '$lib/services/userService';

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

@copilot - is this the only place where getuserbyhandle is used? Are tehre any other places in the code where that is used?


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;
}

Comment on lines +1 to +42

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

The new session.ts module lacks test coverage. The src/lib/auth directory has test files (e.g., bskyVerifyStore.test.ts), indicating this codebase uses comprehensive testing. The session utilities contain critical authentication logic including cookie parsing, JSON validation, and session extraction that should be tested. Consider adding session.test.ts with test cases for valid/invalid session cookies, malformed JSON, missing fields, and the clearSession function.

Copilot uses AI. Check for mistakes.
140 changes: 140 additions & 0 deletions src/lib/services/userService.ts
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;
})();

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

The agentPromise variable is cached at module level (line 17) but is never reset. If the initial agent creation fails, all subsequent calls to getAgent() will return the same rejected promise. Consider resetting agentPromise to null in the catch block of failed initialization attempts, or implementing a retry mechanism with exponential backoff. This prevents permanent failure if the Bluesky service is temporarily unavailable during app initialization.

Suggested change
})();
})().catch((err) => {
agentPromise = null;
throw err;
});

Copilot uses AI. Check for mistakes.
}
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;
Comment on lines +60 to +69

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

The profile is fetched twice in getOrCreateUser: once via resolveDidFromHandle (line 60) and again for display name and avatar (lines 66-69). This makes two API calls for the same data. Consider combining these into a single call by having resolveDidFromHandle return the full profile object, or create a new function that fetches both DID and profile data together. This reduces API calls and improves performance.

Copilot uses AI. Check for mistakes.
} 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
};
}

Comment on lines +1 to +140

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

The new userService.ts module lacks test coverage. Other services in the same directory (accountOverrides.ts, accountsRegistry.ts, bskyService.ts, twitterService.ts) all have corresponding test files. The getOrCreateUser and getUserByHandle functions contain complex logic including external API calls, database operations, and error handling that should be tested. Consider adding userService.test.ts with test cases for successful user creation, DID resolution, profile fetching, database upsert errors, and null Supabase client scenarios.

Copilot uses AI. Check for mistakes.
42 changes: 34 additions & 8 deletions src/routes/api/auth/bsky/create-challenge/+server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 +33

Copilot AI Dec 10, 2025

Copy link

Choose a reason for hiding this comment

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

The handle validation on line 23 checks for '.' or '@' characters, but this validation is too permissive. Valid Bluesky handles should match the format username.domain.tld (e.g., user.bsky.social). The current check would accept invalid formats like .., @@, or a.b (missing TLD). Additionally, the '@' prefix is optional and should be stripped before validation. Consider using a more robust regex pattern like /^@?[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/ or using a Bluesky handle validation library.

Copilot uses AI. Check for mistakes.
}

// If an existing challenge exists and hasn't expired, reuse it (avoid generating multiple codes quickly)
Expand All @@ -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' }
}
);
}
};
Loading
Loading