# Route Spec

## Route ID
`uploads-presign`

## Endpoint
`POST /api/v1/uploads/presign`

## Human Description
Generates a short-lived V4 signed URL so the mobile app uploads binary data directly to GCS (not through Cloud Run). Supports user profile images, hub images, post content images, and PDFs.

## Authentication
- Required: `yes`
- Auth type: `bearer token`
- Required roles/scopes: `authenticated user`

## Request
### Headers
- `Content-Type: application/json`
- `Authorization: Bearer <accessToken>`

### Body
```json
{
  "purpose": "profile_photo",
  "contentType": "image/jpeg",
  "fileName": "IMG_0012.jpg",
  "fileSizeBytes": 734221,
  "checksumSha256": "0f4c2f4f0d1a3f8c7b5e6d4c3b2a19080706050403020100ffeeddccbbaa9988"
}
```

### Validation Rules
- `purpose`: required, enum `profile_photo|hub_photo|post_image|document`.
- `contentType`: required, enum `image/jpeg|image/png|image/webp|application/pdf`.
- `contentType` must match `purpose`: image purposes (`profile_photo|hub_photo|post_image`) accept `image/jpeg|image/png|image/webp`; `document` accepts `application/pdf`.
- `fileName`: required, 1-200 chars.
- `fileSizeBytes`: required, positive integer, max by purpose:
  - `profile_photo|hub_photo|post_image`: `10 MB`
  - `document`: `20 MB`
- `checksumSha256`: required, 64-character SHA-256 hex digest of the file bytes.

## Responses
### Success: `200 OK`
When returned:
- Upload intent is accepted and URL is signed.

Body:
```json
{
  "success": true,
  "message": "Upload URL generated",
  "data": {
    "uploadId": "upl_9fd30c",
    "objectPath": "users/usr_123/profile/upl_9fd30c.jpg",
    "method": "PUT",
    "signedUrl": "https://storage.googleapis.com/...",
    "requiredHeaders": {
      "Content-Type": "image/jpeg",
      "x-goog-content-sha256": "0f4c2f4f0d1a3f8c7b5e6d4c3b2a19080706050403020100ffeeddccbbaa9988"
    },
    "expiresAt": "2026-02-18T12:55:00Z",
    "maxSizeBytes": 10485760
  }
}
```

### Error: `401 Unauthorized`
When returned:
- Missing, invalid, or expired bearer token.

Body:
```json
{"success": false, "error": {"code": "UNAUTHORIZED", "message": "Authentication required.", "details": {}}}
```

### Error: `422 Unprocessable Entity`
When returned:
- Content type, size, or purpose is invalid.

Body:
```json
{
  "success": false,
  "error": {
    "code": "UPLOAD_POLICY_VIOLATION",
    "message": "Upload does not satisfy policy.",
    "details": {
      "allowedContentTypes": ["image/jpeg", "image/png", "image/webp", "application/pdf"]
    }
  }
}
```

## Data & Caching Dependencies
- **Spanner Tables:** `assets (Write/Pending)`
- **Redis Cache:** `None`
- **GCS Storage:** `Upload Bucket (Write Intent)`
- **Edge Cache (CDN):** `No`

## Side Effects
- Creates an upload intent record in `pending` state.
- Binds intent to current user and purpose.

## Idempotency and Retries
- Idempotent: `no`
- Retry guidance: if URL expires or PUT fails, request a new presigned URL.

## Security and Abuse Controls
- Rate limit: `60 presign requests / hour / user`.
- Signed URL TTL: `15 minutes`.
- Service account used for signing must have `roles/storage.objectCreator`.
