# Image URL Fetching — Design Spec

## Overview

Add the ability to fetch images from external URLs via the API, download them to local storage, and preserve the original source URL as metadata. API-only feature — no frontend changes.

## New Endpoint

### `POST /api/images/from-url`

**Request:**

```json
{ "url": "https://example.com/photo.jpg" }
```

**Validation (Zod):**

- `url` — valid URL string, required

**Server-side behavior:**

1. Fetch the URL with a 10-second timeout
2. Check response `Content-Type` is one of: `image/jpeg`, `image/png`, `image/webp`
3. Check `Content-Length` does not exceed 5MB (match existing upload limit)
4. Stream response body to `uploads/` directory using existing naming: `${Date.now()}-${randomUUID()}.${ext}`
5. If any check fails, return 400 with descriptive error

**Response:**

```json
{ "filename": "1712160000000-abc123.jpg", "sourceUrl": "https://example.com/photo.jpg" }
```

Callers can use `filename` for `imageFilename` and `sourceUrl` for `imageSourceUrl` when creating/updating items or candidates.

**Error responses:**

- `400` — invalid URL, unsupported content type, file too large, fetch failed
- `500` — server error during download/save

## Schema Changes

### `items` table

Add column:

```
imageSourceUrl: text("image_source_url")  // nullable
```

### `threadCandidates` table

Add column:

```
imageSourceUrl: text("image_source_url")  // nullable
```

### Zod schemas

Add `imageSourceUrl: z.string().url().optional()` to:

- `createItemSchema`
- `updateItemSchema`
- `createCandidateSchema`
- `updateCandidateSchema`

### Types

Types are inferred from Zod schemas and Drizzle tables — no manual updates needed.

## Existing Behavior Unchanged

- `POST /api/images` (file upload) remains as-is
- All existing image display, cleanup, and serving logic unchanged
- `imageFilename` continues to work identically

## Test Helper Updates

Add `image_source_url TEXT` column to the `items` and `thread_candidates` CREATE TABLE statements in `tests/helpers/db.ts`.

## Testing

- Service test: fetch from a valid URL, verify file saved and filename returned
- Route test: POST to `/api/images/from-url` with valid/invalid URLs
- Validation tests: wrong content type, oversized image, invalid URL format, timeout
