<!-- json
{
  "sourceId": "recipe-upload-presigned",
  "repositoryId": "recipe-manager",
  "filePath": "modules/api/src/web/upload.ts",
  "capabilityTags": ["upload", "presigned-url", "blob"],
  "operationIds": ["enable-file-upload"],
  "applicability": ["sample-project"],
  "notes": [
    "Reference presigned upload URL generation and validation patterns."
  ]
}
-->

# recipe-upload-presigned

- Source: `modules/api/src/web/upload.ts`
- Capability tags: upload, presigned-url, blob
- Applicability: sample-project
- Notes:
  - Reference presigned upload URL generation and validation patterns.

## Reference Code

```typescript
import type { Principal } from '@travetto/auth';
import { Authenticated } from '@travetto/auth-web';
import { Inject } from '@travetto/di';
import { Match, Schema } from '@travetto/schema';
import { ContextParam, Controller, Post } from '@travetto/web';

import type { UploadService } from '../services/upload.ts';

/** Request body for obtaining a presigned upload URL */
@Schema()
export class UploadRequest {
  /** MIME type of the file to be uploaded */
  @Match(/^(image\/(jpeg|png|webp|gif)|application\/pdf|text\/html|text\/plain)$/)
  contentType: string;
}

/** Response containing the presigned URL and the resulting S3 object key */
export type UploadResponse = {
  /** Presigned S3 PUT URL (expires in 5 minutes) */
  uploadUrl: string;
  /** S3 object key; use this as the imageUrl once the upload is complete */
  key: string;
  /** Public URL of the uploaded file */
  imageUrl: string;
};

/** Request body for resolving a single artifact key or URL to a presigned read URL */
@Schema()
export class ResolveArtifactRequest {
  key: string;
}

/**
 * Controller for generating presigned S3 upload URLs for files
 */
@Controller('/upload')
@Authenticated()
export class UploadController {
  @Inject()
  uploadService: UploadService;

  @ContextParam()
  user: Principal;

  /**
   * Generate a presigned S3 PUT URL for a direct file upload.
   * The client should PUT the file binary directly to `uploadUrl`
   * with the matching `Content-Type` header, then use `imageUrl` to reference it.
   */
  @Post('/prepareUpload')
  async postUploadUrl(request: UploadRequest): Promise<UploadResponse> {
    return await this.uploadService.prepareUpload(this.user.id, request.contentType);
  }

  /**
   * Exchange a single artifact key or URL for a fresh presigned S3 read URL.
   */
  @Post('/resolveArtifact')
  async resolveArtifact(request: ResolveArtifactRequest): Promise<{ url: string }> {
    const url = await this.uploadService.resolveReadUrl(this.user.id, request.key);
    return { url };
  }
}
```
