Skip to content
Open
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';

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

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

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

// 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