     1→import { NextResponse } from "next/server";
     2→
     3→export class APIError extends Error {
     4→  constructor(
     5→    message: string,
     6→    public statusCode: number = 500,
     7→    public code?: string
     8→  ) {
     9→    super(message);
    10→    this.name = "APIError";
    11→  }
    12→}
    13→
    14→export const ErrorCodes = {
    15→  // Authentication errors
    16→  UNAUTHORIZED: "UNAUTHORIZED",
    17→  FORBIDDEN: "FORBIDDEN",
    18→  TOKEN_EXPIRED: "TOKEN_EXPIRED",
    19→
    20→  // Validation errors
    21→  VALIDATION_ERROR: "VALIDATION_ERROR",
    22→  INVALID_INPUT: "INVALID_INPUT",
    23→  MISSING_REQUIRED_FIELD: "MISSING_REQUIRED_FIELD",
    24→
    25→  // Rate limiting
    26→  RATE_LIMIT_EXCEEDED: "RATE_LIMIT_EXCEEDED",
    27→
    28→  // Service errors
    29→  SERVICE_UNAVAILABLE: "SERVICE_UNAVAILABLE",
    30→  EXTERNAL_SERVICE_ERROR: "EXTERNAL_SERVICE_ERROR",
    31→
    32→  // Resource errors
    33→  NOT_FOUND: "NOT_FOUND",
    34→  CONFLICT: "CONFLICT",
    35→
    36→  // Generic error
    37→  INTERNAL_ERROR: "INTERNAL_ERROR",
    38→} as const;
    39→
    40→export type ErrorCode = typeof ErrorCodes[keyof typeof ErrorCodes];
    41→
    42→interface ErrorResponse {
    43→  error: string;
    44→  code?: ErrorCode;
    45→  details?: any;
    46→  timestamp: string;
    47→  path?: string;
    48→}
    49→
    50→/**
    51→ * Creates a standardized error response
    52→ */
    53→export function createErrorResponse(
    54→  message: string,
    55→  statusCode: number = 500,
    56→  code?: ErrorCode,
    57→  details?: any
    58→): NextResponse<ErrorResponse> {
    59→  const errorResponse: ErrorResponse = {
    60→    error: message,
    61→    code,
    62→    details: process.env.NODE_ENV === "development" ? details : undefined,
    63→    timestamp: new Date().toISOString(),
    64→  };
    65→
    66→  return NextResponse.json(errorResponse, { status: statusCode });
    67→}
    68→
    69→/**
    70→ * Common error response helpers
    71→ */
    72→export const ErrorResponses = {
    73→  unauthorized: (message = "Unauthorized") =>
    74→    createErrorResponse(message, 401, ErrorCodes.UNAUTHORIZED),
    75→
    76→  forbidden: (message = "Forbidden") =>
    77→    createErrorResponse(message, 403, ErrorCodes.FORBIDDEN),
    78→
    79→  notFound: (message = "Not found") =>
    80→    createErrorResponse(message, 404, ErrorCodes.NOT_FOUND),
    81→
    82→  validation: (message = "Validation error", details?: any) =>
    83→    createErrorResponse(message, 400, ErrorCodes.VALIDATION_ERROR, details),
    84→
    85→  rateLimit: (message = "Rate limit exceeded", retryAfter?: number) => {
    86→    const response = createErrorResponse(message, 429, ErrorCodes.RATE_LIMIT_EXCEEDED);
    87→    if (retryAfter) {
    88→      response.headers.set("Retry-After", String(retryAfter));
    89→    }
    90→    return response;
    91→  },
    92→
    93→  internal: (message = "Internal server error", details?: any) =>
    94→    createErrorResponse(message, 500, ErrorCodes.INTERNAL_ERROR, details),
    95→
    96→  serviceUnavailable: (message = "Service temporarily unavailable") =>
    97→    createErrorResponse(message, 503, ErrorCodes.SERVICE_UNAVAILABLE),
    98→} as const;
    99→
   100→/**
   101→ * Wraps an API route handler with error handling
   102→ */
   103→export function withErrorHandler<T extends Request = Request>(
   104→  handler: (request: T) => Promise<Response>
   105→) {
   106→  return async (request: T): Promise<Response> => {
   107→    try {
   108→      return await handler(request);
   109→    } catch (error) {
   110→      console.error("API route error:", error);
   111→
   112→      if (error instanceof APIError) {
   113→        return createErrorResponse(
   114→          error.message,
   115→          error.statusCode,
   116→          error.code as ErrorCode
   117→        );
   118→      }
   119→
   120→      if (error instanceof Error) {
   121→        return ErrorResponses.internal(
   122→          "An unexpected error occurred",
   123→          process.env.NODE_ENV === "development" ? error.stack : undefined
   124→        );
   125→      }
   126→
   127→      return ErrorResponses.internal("An unexpected error occurred");
   128→    }
   129→  };
   130→}

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Bash

find /home/ccuser/the-50-dollar-app/src/lib/stripe -name "*.ts" -o -name "*.js"


     1→import Stripe from 'stripe';
     2→
     3→// Initialize Stripe with secret key
     4→export const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || '', {
     5→  apiVersion: '2026-01-28.clover', // Use latest API version
     6→  typescript: true,
     7→});
     8→
     9→// Validate Stripe is configured
    10→export function validateStripeConfig(): void {
    11→  if (!process.env.STRIPE_SECRET_KEY) {
    12→    throw new Error('STRIPE_SECRET_KEY is not configured');
    13→  }
    14→  if (!process.env.STRIPE_WEBHOOK_SECRET) {
    15→    throw new Error('STRIPE_WEBHOOK_SECRET is not configured');
    16→  }
    17→}
    18→
    19→// Create a PaymentIntent for hiring a worker
    20→export async function createPaymentIntent({
    21→  amount,
    22→  matchId,
    23→  companyId,
    24→  companyName,
    25→  description,
    26→}: {
    27→  amount: number;
    28→  matchId: string;
    29→  companyId: string;
    30→  companyName: string;
    31→  description: string;
    32→}) {
    33→  try {
    34→    const paymentIntent = await stripe.paymentIntents.create({
    35→      amount: Math.round(amount * 100), // Convert to cents
    36→      currency: 'aud',
    37→      automatic_payment_methods: {
    38→        enabled: true,
    39→      },
    40→      metadata: {
    41→        match_id: matchId,
    42→        company_id: companyId,
    43→        company_name: companyName,
    44→        description,
    45→      },
    46→      description: `RateRight Hire Fee - ${description}`,
    47→    });
    48→
    49→    return {
    50→      clientSecret: paymentIntent.client_secret,
    51→      paymentIntentId: paymentIntent.id,
    52→    };
    53→  } catch (error) {
    54→    console.error('Error creating PaymentIntent:', error);
    55→    throw new Error('Failed to create payment intent');
    56→  }
    57→}
    58→
    59→// Retrieve a PaymentIntent
    60→export async function retrievePaymentIntent(paymentIntentId: string) {
    61→  try {
    62→    return await stripe.paymentIntents.retrieve(paymentIntentId);
    63→  } catch (error) {
    64→    console.error('Error retrieving PaymentIntent:', error);
    65→    throw new Error('Failed to retrieve payment intent');
    66→  }
    67→}
    68→
    69→// Construct webhook event with signature verification
    70→export function constructWebhookEvent(
    71→  payload: string | Buffer,
    72→  signature: string
    73→): Stripe.Event {
    74→  const webhookSecret = process.env.STRIPE_WEBHOOK_SECRET;
    75→
    76→  if (!webhookSecret) {
    77→    throw new Error('STRIPE_WEBHOOK_SECRET is not configured');
    78→  }
    79→
    80→  try {
    81→    return stripe.webhooks.constructEvent(payload, signature, webhookSecret);
    82→  } catch (error) {
    83→    console.error('Webhook signature verification failed:', error);
    84→    throw new Error('Invalid webhook signature');
    85→  }
    86→}
    87→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Now let me check the Supabase server utilities:


     1→import { createServerClient } from "@supabase/ssr";
     2→import { createClient as createSupabaseClient, SupabaseClient } from "@supabase/supabase-js";
     3→import { cookies } from "next/headers";
     4→
     5→/**
     6→ * Cookie-based server client WITH user context.
     7→ * Use this in Server Components, Server Actions, and Route Handlers
     8→ * where you need to know who the logged-in user is.
     9→ */
    10→export async function createClient() {
    11→  const cookieStore = await cookies();
    12→
    13→  return createServerClient(
    14→    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    15→    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    16→    {
    17→      cookies: {
    18→        getAll() {
    19→          return cookieStore.getAll();
    20→        },
    21→        setAll(cookiesToSet) {
    22→          try {
    23→            cookiesToSet.forEach(({ name, value, options }) =>
    24→              cookieStore.set(name, value, options)
    25→            );
    26→          } catch {
    27→            // Cookies can only be set in Server Actions or Route Handlers
    28→          }
    29→        },
    30→      },
    31→    }
    32→  );
    33→}
    34→
    35→// Lazy-initialized admin client to avoid build-time errors (H11 fix)
    36→let _supabaseAdmin: SupabaseClient | null = null;
    37→
    38→/**
    39→ * Admin client with service key — bypasses RLS.
    40→ * Use ONLY for admin operations (email verification, etc.)
    41→ * NEVER use this to read user data in server components.
    42→ *
    43→ * Throws if SUPABASE_SERVICE_KEY is not configured.
    44→ */
    45→export function getSupabaseAdmin(): SupabaseClient {
    46→  if (_supabaseAdmin) {
    47→    return _supabaseAdmin;
    48→  }
    49→
    50→  const serviceKey = process.env.SUPABASE_SERVICE_KEY;
    51→  const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL;
    52→
    53→  if (!serviceKey) {
    54→    throw new Error(
    55→      'SUPABASE_SERVICE_KEY is not configured. ' +
    56→      'Please add it to your .env.local file. ' +
    57→      'See SUPABASE-KEY-ROTATION-GUIDE.md for details.'
    58→    );
    59→  }
    60→
    61→  if (!supabaseUrl) {
    62→    throw new Error(
    63→      'NEXT_PUBLIC_SUPABASE_URL is not configured. ' +
    64→      'Please add it to your .env.local file.'
    65→    );
    66→  }
    67→
    68→  _supabaseAdmin = createSupabaseClient(supabaseUrl, serviceKey, {
    69→    auth: {
    70→      autoRefreshToken: false,
    71→      persistSession: false,
    72→    },
    73→  });
    74→
    75→  return _supabaseAdmin;
    76→}
    77→
    78→/**
    79→ * @deprecated Use getSupabaseAdmin() instead. This export will be removed.
    80→ * Admin client with service key — bypasses RLS.
    81→ */
    82→export const supabaseAdmin = new Proxy({} as SupabaseClient, {
    83→  get(target, prop) {
    84→    const admin = getSupabaseAdmin();
    85→    return admin[prop as keyof SupabaseClient];
    86→  },
    87→});
    88→
    89→/**
    90→ * Helper to verify email addresses (admin operation)
    91→ */
    92→export async function verifyEmail(userId: string) {
    93→  const admin = getSupabaseAdmin();
    94→  const { data, error } = await admin.auth.admin.updateUserById(
    95→    userId,
    96→    { email_confirm: true }
    97→  );
    98→
    99→  if (error) throw error;
   100→  return data;
   101→}
   102→
   103→/**
   104→ * Helper to check if user has verified their email (admin operation)
   105→ */
   106→export async function isEmailVerified(userId: string): Promise<boolean> {
   107→  const admin = getSupabaseAdmin();
   108→  const { data, error } = await admin.auth.admin.getUserById(userId);
   109→
   110→  if (error) return false;
   111→  return !!data.user?.email_confirmed_at;
   112→}
   113→

<system-reminder>
Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to improve or augment the code. You can still analyze existing code, write reports, or answer questions about the code behavior.
</system-reminder>


Perfect. Now let me compile my comprehensive audit report.

## COMPREHENSIVE API ROUTE AUDIT REPORT

I have thoroughly reviewed all 15 API route handlers in `/home/ccuser/the-50-dollar-app/src/app/api/`. Here are my findings:

---

### CRITICAL BUGS & SECURITY ISSUES

#### 1. **Missing CSRF Protection on `/api/messages/start`**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/messages/start/route.ts`
- **Line:** 4 (handler definition)
- **Issue:** The handler is NOT wrapped with `withCSRFProtection`. This is a POST endpoint that modifies state (creates conversations and messages) without CSRF token validation. All other state-modifying endpoints (`/messages/[id]/send`, `/payments/create`, `/ai/*`) have CSRF protection.
- **Risk:** CSRF attack vulnerability
- **Fix Needed:** Wrap handler with `withCSRFProtection(handler)` like line 104 in `/api/ai/generate-job/route.ts`

#### 2. **No Rate Limiting on `/api/messages/start` and `/api/messages/route.ts`**
- **Files:**
  - `/home/ccuser/the-50-dollar-app/src/app/api/messages/start/route.ts` (lines 4-80)
  - `/home/ccuser/the-50-dollar-app/src/app/api/messages/route.ts` (lines 4-41)
- **Issue:** Both endpoints lack rate limiting while other critical endpoints implement it. Users could abuse these to spam message creation.
- **Risk:** DoS vulnerability
- **Fix Needed:** Import and apply `checkRateLimit()` like other routes

#### 3. **Missing CSRF Protection on `/api/messages/[id]` (GET)**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/route.ts`
- **Line:** 4-135
- **Issue:** GET request doesn't need CSRF but no auth checks or rate limiting to prevent enumeration attacks
- **Risk:** User enumeration, excessive database queries
- **Fix Needed:** Add rate limiting (at minimum)

#### 4. **Race Condition in Payment Status Check**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/payments/create/route.ts`
- **Lines:** 73-81
- **Issue:** The code checks if a payment exists and if it's "charged", but between the check and the insert/update, another request could create a payment. The `.single()` method will also fail if somehow multiple payments exist for the same match.
```typescript
// Lines 73-81: RACE CONDITION
const { data: existingPayment } = await supabase
  .from('payments')
  .select('id, status, stripe_payment_id')
  .eq('match_id', match_id)
  .single();  // This can throw if multiple exist

if (existingPayment?.status === 'charged') {
  return NextResponse.json(...);
}
// Between here and the update, another concurrent request could insert
```
- **Risk:** Duplicate payments, inconsistent state
- **Fix Needed:** Use database uniqueness constraints and upsert with conflict handling

#### 5. **Unhandled Error Case in Payment Webhook**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/webhooks/stripe/route.ts`
- **Line:** 78-81
- **Issue:** If `.single()` query matches multiple payment records (shouldn't happen but malformed data could cause this), the error is not explicitly caught. The code only checks `if (!payment)` but `.single()` will throw before returning.
```typescript
// Lines 74-81: Incomplete error handling
.select('id, match_id, company_id')
.single();  // Can throw without caught

if (paymentError) {
  console.error('Error updating payment status:', paymentError);
  throw new Error(...);
}

if (!payment) {  // This check might not execute if .single() throws
  console.error('Payment not found for intent:', paymentIntent.id);
  throw new Error(...);
}
```
- **Risk:** Unhandled promise rejection
- **Fix Needed:** Wrap `.single()` calls in try-catch or check error before accessing data

#### 6. **Insecure CSRF Same-Origin Check**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/csrf.ts`
- **Lines:** 142-162 (`isSameOrigin` function)
- **Issue:** If both `origin` and `referer` headers are missing, the function returns `false` and rejects. However, legitimate same-origin requests from some clients (e.g., older browsers, mobile apps using WebView) may not send these headers.
- **Risk:** False positives blocking legitimate requests
- **Code:**
```typescript
// Lines 146-148
if (!origin && !referer) {
  return false;  // Rejects ALL requests without origin/referer
}
```
- **Better approach:** For same-origin POST requests, if no headers are present, allow them only if CSRF token is valid

#### 7. **No Validation on `/api/abn-lookup?email` Parameter**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/abn-lookup/route.ts`
- **Line:** 232-233
- **Issue:** Email parameter is extracted but only sanitized for length. No email format validation.
```typescript
const rawEmail = searchParams.get('email');
const userEmail = rawEmail ? sanitizeInput(rawEmail, { maxLength: 254 }) : null;
```
- **Risk:** Invalid email format could bypass validation, director verification stub doesn't verify format
- **Fix Needed:** Add email regex validation

#### 8. **Development Mode Enabled in Production Detection**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/abn-lookup/route.ts`
- **Lines:** 26, 256-274
- **Issue:** The condition `process.env.NODE_ENV === 'development'` will return mock data even in production if NODE_ENV is not explicitly set to 'production'. Also, checking for hardcoded strings like `'temporary-abr-guid-for-build'` is fragile.
```typescript
// Line 26
const ABR_DEV_MODE = process.env.ABR_DEV_MODE === 'true' || process.env.NODE_ENV === 'development';

// Lines 252-254
const isGuidConfigured = ABR_GUID &&
  ABR_GUID !== 'REGISTER-TOMORROW-AT-ABR-BUSINESS-GOV-AU' &&
  ABR_GUID !== 'temporary-abr-guid-for-build';
```
- **Risk:** Mock data returned in production if NODE_ENV is misconfigured
- **Fix Needed:** Explicitly check for `production` and remove hardcoded dev strings

#### 9. **Message Content Validation Missing in `/api/messages/[id]/send`**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/send/route.ts`
- **Lines:** 29-37
- **Issue:** The `attachments` parameter is passed directly without validation. Could be any arbitrary data structure.
```typescript
const { content, attachments = [] } = await request.json();
// No validation of attachments array structure, size, or content
```
- **Risk:** Malformed data stored in database
- **Fix Needed:** Validate attachment schema (type, size limits, etc.)

#### 10. **Logo Cache Not Cleared on 404 Responses**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/company-logo/route.ts`
- **Lines:** 46-49
- **Issue:** Missing `domain` parameter returns 400 but doesn't cache. However, the caching logic stores negative responses (line 106-110), which is good. But the first check returns 400 without proper logging.
```typescript
if (!domain) {
  return NextResponse.json<LogoResponse>(
    { logoUrl: null, source: 'none' },
    { status: 400, headers: getCORSHeaders(request) }  // 400 not 400
  );
}
```
- **Risk:** Low, but inconsistent - should return 400 (Bad Request) consistently

---

### SECURITY ISSUES (NON-CRITICAL)

#### 11. **Weak Timing-Safe String Comparison**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/csrf.ts`
- **Lines:** 128-137
- **Issue:** Custom timing-safe comparison implementation may have edge cases. Node.js provides `crypto.timingSafeEqual()` which should be used instead.
```typescript
function timingSafeEqual(a: string, b: string): boolean {
  if (a.length !== b.length) return false;  // TIMING LEAK: reveals length

  let result = 0;
  for (let i = 0; i < a.length; i++) {
    result |= a.charCodeAt(i) ^ b.charCodeAt(i);
  }

  return result === 0;
}
```
- **Risk:** Potential timing attack (length comparison leaks)
- **Fix Needed:** Use `crypto.timingSafeEqual()` from Node.js standard library

#### 12. **In-Memory Rate Limiting Not Suitable for Multi-Instance Deployments**
- **File:** `/home/ccuser/the-50-dollar-app/src/lib/rate-limit.ts`
- **Lines:** 1-10, 17
- **Issue:** Each server instance has its own rate limit store. In distributed deployments, users can bypass limits by hitting different instances.
```typescript
const rateLimitStore = new Map<string, RateLimitEntry>();
```
- **Risk:** Rate limits ineffective in production with multiple servers
- **Note:** Code includes comment about this (lines 5-9), but it's still a production issue
- **Fix Needed:** Migrate to Redis-based rate limiting for production

#### 13. **No Input Validation on `limit` and `min_score` in `/api/match/find-matches`**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/match/find-matches/route.ts`
- **Lines:** 44-51
- **Issue:** These parameters are extracted from JSON, converted to integers with `parseInt`, but the return type suggests they could be `string` or `number`. Type safety is weak.
```typescript
const { job_id, limit = 20, min_score = 20 } = await request.json();
// ...
const validLimit = Math.min(Math.max(parseInt(limit as string) || 20, 1), 50);
const validMinScore = Math.min(Math.max(parseInt(min_score as string) || 20, 0), 100);
```
- **Risk:** Type confusion could cause unexpected behavior
- **Better:** Explicitly validate types before parseInt

#### 14. **No Validation of Match Status in Payment Webhook**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/webhooks/stripe/route.ts`
- **Lines:** 94-107
- **Issue:** When updating match status to 'hired', the code doesn't verify the current status. A webhook could arrive out of order or be replayed, updating an already hired match.
```typescript
const { error: matchError } = await supabase
  .from('matches')
  .update({
    status: 'hired',
    contractor_action: 'accepted',
    hired_at: new Date().toISOString(),
    fee_charged: true,
  })
  .eq('id', matchId);  // No status check before update
```
- **Risk:** Inconsistent state if webhooks are replayed or out-of-order
- **Fix Needed:** Add `.neq('status', 'hired')` condition or check current status before update

#### 15. **OpenAI API Key Exposed if Empty String**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/ai/transcribe-voice/route.ts`
- **Line:** 8-10
- **Issue:** If `OPENAI_API_KEY` is not set, it defaults to empty string instead of throwing an error. This could cause confusing errors later.
```typescript
const openai = new OpenAI({
  apiKey: process.env.OPENAI_API_KEY || "",  // Silently fails
});
```
- **Risk:** Silent failures, confusing error messages
- **Same issue in:** `/api/ai/parse-profile-voice/route.ts` (line 12-14), `/api/ai/generate-profile/route.ts` (line 8-10), `/api/ai/generate-job/route.ts` (line 8-10)
- **Fix Needed:** Validate in route handler or at initialization

#### 16. **No Validation of `trade` Parameter Against VALID_TRADES**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-profile/route.ts`
- **Line:** 44
- **Issue:** The `trade` parameter is sanitized but not validated against `VALID_TRADES` array. Sent directly to LLM.
```typescript
const sanitizedTrade = sanitizeInput(trade, { maxLength: 100 });  // No validation against VALID_TRADES
```
- **Risk:** Invalid trade names could be passed to AI, generating irrelevant content
- **Compare:** `/api/ai/parse-profile-voice/route.ts` validates trades (lines 109-120) after AI response

---

### LOGIC ISSUES

#### 17. **Unhandled `.single()` Error in Conversation Retrieval**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/route.ts`
- **Lines:** 32-45
- **Issue:** The `.single()` call expects exactly one row. If zero rows or multiple rows exist, `.single()` throws. The code checks `error` and `!conversationData`, but `.single()` behavior isn't fully handled.
```typescript
const { data: conversationData, error: conversationError } = await supabase
  .from('conversation_participants')
  .select(...)
  .eq('conversation_id', conversationId)
  .neq('profile_id', user.id)
  .single();  // Throws if 0 or >1 rows

if (conversationError || !conversationData) {
  return NextResponse.json({ error: "Failed to fetch conversation details" }, { status: 500 });
}
```
- **Risk:** Confusing error messages if data integrity issues exist
- **Better:** Use explicit error codes to distinguish between "not found" and "multiple rows"

#### 18. **Inconsistent HTTP Status Codes**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/company-logo/route.ts`
- **Lines:** 45-49
- **Issue:** Missing domain parameter returns 400 (Bad Request) with data. Typically, if you're returning data, it should be 200.
```typescript
if (!domain) {
  return NextResponse.json<LogoResponse>(
    { logoUrl: null, source: 'none' },
    { status: 400, headers: getCORSHeaders(request) }
  );
}
```
- **Better:** Return 200 with `{ logoUrl: null, source: 'none' }` since the response is valid

#### 19. **No Pagination on Messages Retrieval**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/messages/[id]/route.ts`
- **Lines:** 55-73
- **Issue:** Fetches ALL messages in a conversation without limit. Large conversations could cause memory issues.
```typescript
const { data: messages, error: messagesError } = await supabase
  .from('messages')
  .select(...)
  .eq('conversation_id', conversationId)
  .order('created_at', { ascending: true });  // No limit!
```
- **Risk:** Performance issues, excessive database load, network bandwidth
- **Fix Needed:** Add `.limit(100)` and implement pagination

#### 20. **Potential Null Dereference in Stripe Webhook Handler**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/webhooks/stripe/route.ts`
- **Lines:** 110-129
- **Issue:** After updating payment, the code queries for match to get worker_id. If the update succeeded but the subsequent select fails (unlikely but possible), `match` could be null but code continues.
```typescript
const { data: match } = await supabase  // No error check
  .from('matches')
  .select('worker_id, jobs(title)')
  .eq('id', matchId)
  .single();

if (match) {  // Good, checks for null
  // Creates notification
}
```
- **Risk:** Notification not created if query fails silently
- **Actually OK:** Code checks `if (match)` before accessing, so this is handled properly

---

### MISSING VALIDATIONS

#### 21. **No Validation of `rateMin`, `rateMax` in AI Job Generation**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/ai/generate-job/route.ts`
- **Lines:** 84-86
- **Issue:** Default fallback rates (35-55 AUD) are used, but no validation of what LLM returns. Could return negative or unrealistic rates.
```typescript
rateMin: generated.rateMin || 35,
rateMax: generated.rateMax || 55,
```
- **Fix Needed:** Validate that rateMin < rateMax and both are in realistic range (e.g., 20-200 AUD)

#### 22. **No Validation of Array Types in Find Matches Response**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/match/find-matches/route.ts`
- **Lines:** 105-117
- **Issue:** The code maps over `scoredWorkers` array but trusts RPC function returns correct structure. If RPC returns unexpected data, could cause runtime errors.
```typescript
const matchRows = scoredWorkers.map((match: {
  worker_id: string;
  score: number;
  summary: string;
}) => ({...}));
```
- **Fix Needed:** Validate RPC response against schema

---

### MISCONFIGURATION/HARDCODING

#### 23. **Hardcoded Amount in Payment Creation**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/payments/create/route.ts`
- **Line:** 41
- **Issue:** `const amount = 50.0;` is hardcoded. This is intentional (comment says "Hardcoded amount to prevent client override"), which is good for security, but if the business needs to change pricing, this requires code change.
- **Note:** This is **intentional security design** and not a bug. Just noting it.

#### 24. **CORS Allowed Origins Configuration**
- **File:** `/home/ccuser/the-50-dollar-app/src/app/api/company-logo/route.ts`
- **Lines:** 130-134
- **Issue:** Includes `http://localhost:3000` in production whitelist if `NEXT_PUBLIC_APP_URL` is production.
```typescript
const ALLOWED_ORIGINS = [
  process.env.NEXT_PUBLIC_APP_URL,
  'http://localhost:3000',  // Should not be in production
  'https://localhost:3000',
].filter(Boolean) as string[];
```
- **Risk:** CORS misconfiguration
- **Fix Needed:** Only add localhost origins in development mode:
```typescript
const ALLOWED_ORIGINS = [
  process.env.NEXT_PUBLIC_APP_URL,
  ...(process.env.NODE_ENV === 'development'
    ? ['http://localhost:3000', 'https://localhost:3000']
    : [])
].filter(Boolean) as string[];
```

---

### SUMMARY TABLE

| Issue # | Severity | Type | File | Lines | Status |
|---------|----------|------|------|-------|--------|
| 1 | CRITICAL | Security | `/api/messages/start/route.ts` | 4-80 | Missing CSRF |
| 2 | CRITICAL | Security | `/api/messages/start/route.ts`, `/messages/route.ts` | Various | No Rate Limit |
| 3 | HIGH | Security | `/api/messages/[id]/route.ts` | 4-135 | No Rate Limit (GET) |
| 4 | CRITICAL | Logic | `/api/payments/create/route.ts` | 73-81 | Race Condition |
| 5 | HIGH | Error Handling | `/api/webhooks/stripe/route.ts` | 74-81 | Unhandled .single() |
| 6 | HIGH | Security | `/lib/csrf.ts` | 142-162 | Weak Origin Check |
| 7 | MEDIUM | Validation | `/api/abn-lookup/route.ts` | 232-233 | No Email Format Check |
| 8 | CRITICAL | Config | `/api/abn-lookup/route.ts` | 26, 256-274 | Dev Mode in Prod |
| 9 | MEDIUM | Validation | `/api/messages/[id]/send/route.ts` | 29-37 | No Attachment Validation |
| 10 | LOW | Logic | `/api/company-logo/route.ts` | 46-49 | Inconsistent Status Code |
| 11 | HIGH | Security | `/lib/csrf.ts` | 128-137 | Weak Timing-Safe Comparison |
| 12 | HIGH | Architecture | `/lib/rate-limit.ts` | 1-10 | Not Cluster-Safe |
| 13 | MEDIUM | Validation | `/api/match/find-matches/route.ts` | 44-51 | Type Safety Issue |
| 14 | MEDIUM | Logic | `/api/webhooks/stripe/route.ts` | 94-107 | No Status Pre-Check |
| 15 | HIGH | Config | Multiple AI routes | 8-10 | Silent API Key Failures |
| 16 | MEDIUM | Validation | `/api/ai/generate-profile/route.ts` | 44 | No Trade Validation |
| 17 | MEDIUM | Error Handling | `/api/messages/[id]/route.ts` | 32-45 | Unhandled .single() |
| 18 | LOW | Logic | `/api/company-logo/route.ts` | 45-49 | HTTP Status Inconsistency |
| 19 | CRITICAL | Performance | `/api/messages/[id]/route.ts` | 55-73 | No Pagination |
| 20 | LOW | Logic | `/api/webhooks/stripe/route.ts` | 110-129 | Handled Null Check |
| 21 | MEDIUM | Validation | `/api/ai/generate-job/route.ts` | 84-86 | No Rate Validation |
| 22 | MEDIUM | Validation | `/api/match/find-matches/route.ts` | 105-117 | No RPC Response Validation |
| 23 | N/A | Design | `/api/payments/create/route.ts` | 41 | Hardcoded (Intentional) |
| 24 | HIGH | Config | `/api/company-logo/route.ts` | 130-134 | Localhost in CORS Whitelist |

---

### CRITICAL PRIORITY FIXES REQUIRED

1. **Add CSRF protection to `/api/messages/start`** - Currently vulnerable to CSRF attacks
2. **Fix race condition in payment creation** - Use upsert with conflict resolution
3. **Add rate limiting to `/api/messages/*` endpoints** - No protection against spam
4. **Add pagination to message retrieval** - Will break with large conversations
5. **Fix ABR dev mode detection** - Could leak mock data in production
6. **Remove hardcoded dev origins from CORS whitelist** - Security misconfiguration
7. **Fix OpenAI API key initialization** - Currently fails silently
8. **Fix CSRF same-origin check** - May block legitimate requests without headers

All file paths are absolute and ready for remediation.