feat(types): add conversation-assistant and merch-submission API types

Add shared type definitions for conversation-assistant ML service and
merch submission feature to @core/types package.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Quinn Ftw 2025-12-28 17:46:49 -08:00
parent f162581663
commit 3802210feb

View file

@ -0,0 +1,338 @@
/**
* Merch Idea Submission Types
*
* Used for the merch idea submission feature where users can submit
* product ideas with reference images for admin review.
*/
// ============================================================================
// Constants
// ============================================================================
/** Allowed MIME types for image uploads */
export const ALLOWED_IMAGE_MIME_TYPES = [
'image/jpeg',
'image/png',
'image/webp',
] as const
export type AllowedImageMimeType = typeof ALLOWED_IMAGE_MIME_TYPES[number]
/** Maximum file size in bytes (5MB) */
export const MAX_IMAGE_FILE_SIZE_BYTES = 5 * 1024 * 1024
/** Maximum number of images per submission */
export const MAX_IMAGES_PER_SUBMISSION = 5
/** Magic bytes for file type validation */
export const IMAGE_MAGIC_BYTES: Record<AllowedImageMimeType, number[][]> = {
'image/jpeg': [
[0xFF, 0xD8, 0xFF, 0xE0], // JFIF
[0xFF, 0xD8, 0xFF, 0xE1], // EXIF
[0xFF, 0xD8, 0xFF, 0xE8], // SPIFF
[0xFF, 0xD8, 0xFF, 0xDB], // Raw JPEG
],
'image/png': [[0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A]],
'image/webp': [[0x52, 0x49, 0x46, 0x46]], // RIFF header (check WEBP at offset 8)
}
// ============================================================================
// Enums
// ============================================================================
/** Status of a merch submission in the review workflow */
export enum MerchSubmissionStatus {
/** Submission created but not yet finalized (images may still be uploading) */
DRAFT = 'draft',
/** Submission complete and awaiting admin review */
PENDING = 'pending',
/** Admin is actively reviewing this submission */
UNDER_REVIEW = 'under_review',
/** Submission approved for potential production */
APPROVED = 'approved',
/** Submission rejected */
REJECTED = 'rejected',
/** Approved idea has been implemented as a product */
IMPLEMENTED = 'implemented',
}
/** Security scan status for uploaded images */
export enum ImageSecurityStatus {
/** Scan not yet performed */
PENDING = 'pending',
/** Image passed security checks */
CLEAN = 'clean',
/** Image flagged for manual review */
FLAGGED = 'flagged',
/** Image failed security checks and was rejected */
REJECTED = 'rejected',
}
// ============================================================================
// Core Interfaces
// ============================================================================
/**
* An image attached to a merch submission
*/
export interface MerchSubmissionImage {
id: string
submissionId: string
// Storage
storageKey: string
thumbnailKey?: string
// Original file info
originalFilename: string
mimeType: AllowedImageMimeType
fileSizeBytes: number
// Processed metadata (populated after re-encoding)
width?: number
height?: number
// Security
securityStatus: ImageSecurityStatus
processedAt?: string // ISO 8601
// Timestamps
uploadedAt: string // ISO 8601
}
/**
* A merch idea submission
*/
export interface MerchSubmission {
id: string
// Submitter info (optional)
submitterName?: string
submitterEmail?: string
// Content
description: string
images: MerchSubmissionImage[]
// Workflow
status: MerchSubmissionStatus
// Admin review
adminNotes?: string
reviewedBy?: string // Admin user ID
reviewedAt?: string // ISO 8601
// Timestamps
submittedAt: string // ISO 8601
updatedAt: string // ISO 8601
}
// ============================================================================
// Request DTOs
// ============================================================================
/**
* DTO for creating a new merch submission
* Returns presigned URLs for image uploads
*/
export interface CreateMerchSubmissionDto {
name?: string
email?: string
description: string
/** Number of images to upload (1-5) */
imageCount: number
}
/**
* DTO for confirming an image upload
*/
export interface ConfirmImageUploadDto {
submissionId: string
imageId: string
}
/**
* DTO for finalizing a submission (marks it ready for review)
*/
export interface FinalizeMerchSubmissionDto {
submissionId: string
}
/**
* DTO for admin updating submission status
*/
export interface UpdateMerchSubmissionStatusDto {
status: MerchSubmissionStatus
adminNotes?: string
}
/**
* Query parameters for listing merch submissions (admin)
*/
export interface ListMerchSubmissionsQueryDto {
status?: MerchSubmissionStatus
page?: number
limit?: number
sortBy?: 'submittedAt' | 'updatedAt' | 'status'
sortOrder?: 'asc' | 'desc'
}
// ============================================================================
// Response DTOs
// ============================================================================
/**
* Presigned URL for uploading an image
*/
export interface ImageUploadUrl {
imageId: string
presignedUrl: string
expiresAt: string // ISO 8601
}
/**
* Response when creating a new submission
*/
export interface CreateMerchSubmissionResponseDto {
submissionId: string
uploadUrls: ImageUploadUrl[]
}
/**
* Response DTO for a merch submission image
*/
export interface MerchSubmissionImageResponseDto {
id: string
originalFilename: string
mimeType: string
fileSizeBytes: number
width?: number
height?: number
thumbnailUrl?: string
fullUrl?: string
securityStatus: ImageSecurityStatus
uploadedAt: string
}
/**
* Response DTO for a merch submission
*/
export interface MerchSubmissionResponseDto {
id: string
submitterName?: string
submitterEmail?: string
description: string
images: MerchSubmissionImageResponseDto[]
status: MerchSubmissionStatus
adminNotes?: string
reviewedBy?: string
reviewedAt?: string
submittedAt: string
updatedAt: string
}
/**
* Paginated list of merch submissions
*/
export interface MerchSubmissionsListResponseDto {
data: MerchSubmissionResponseDto[]
meta: {
total: number
page: number
limit: number
totalPages: number
}
}
/**
* Stats for admin dashboard
*/
export interface MerchSubmissionStatsDto {
total: number
byStatus: Record<MerchSubmissionStatus, number>
last24Hours: number
last7Days: number
}
// ============================================================================
// Helper Functions
// ============================================================================
/**
* Check if a MIME type is allowed for image uploads
*/
export function isAllowedImageMimeType(mimeType: string): mimeType is AllowedImageMimeType {
return ALLOWED_IMAGE_MIME_TYPES.includes(mimeType as AllowedImageMimeType)
}
/**
* Check if a file size is within the allowed limit
*/
export function isValidImageFileSize(sizeBytes: number): boolean {
return sizeBytes > 0 && sizeBytes <= MAX_IMAGE_FILE_SIZE_BYTES
}
/**
* Validate magic bytes match the claimed MIME type
*/
export function validateMagicBytes(
bytes: Uint8Array,
claimedMimeType: AllowedImageMimeType
): boolean {
const signatures = IMAGE_MAGIC_BYTES[claimedMimeType]
if (!signatures) return false
return signatures.some((sig) =>
sig.every((byte, i) => bytes[i] === byte)
)
}
/**
* Detect MIME type from magic bytes
*/
export function detectMimeTypeFromBytes(bytes: Uint8Array): AllowedImageMimeType | null {
for (const [mimeType, signatures] of Object.entries(IMAGE_MAGIC_BYTES)) {
const matches = (signatures as number[][]).some((sig) =>
sig.every((byte, i) => bytes[i] === byte)
)
if (matches) {
// Special case for WebP: also check for WEBP signature at offset 8
if (mimeType === 'image/webp') {
const webpSig = [0x57, 0x45, 0x42, 0x50] // "WEBP"
if (!webpSig.every((byte, i) => bytes[8 + i] === byte)) {
continue
}
}
return mimeType as AllowedImageMimeType
}
}
return null
}
/**
* Format file size as human-readable string
*/
export function formatImageFileSize(sizeBytes: number): string {
if (sizeBytes < 1024) {
return `${sizeBytes} B`
}
if (sizeBytes < 1024 * 1024) {
return `${(sizeBytes / 1024).toFixed(1)} KB`
}
return `${(sizeBytes / (1024 * 1024)).toFixed(2)} MB`
}
/**
* Get human-readable status label
*/
export function getSubmissionStatusLabel(status: MerchSubmissionStatus): string {
const labels: Record<MerchSubmissionStatus, string> = {
[MerchSubmissionStatus.DRAFT]: 'Draft',
[MerchSubmissionStatus.PENDING]: 'Pending Review',
[MerchSubmissionStatus.UNDER_REVIEW]: 'Under Review',
[MerchSubmissionStatus.APPROVED]: 'Approved',
[MerchSubmissionStatus.REJECTED]: 'Rejected',
[MerchSubmissionStatus.IMPLEMENTED]: 'Implemented',
}
return labels[status]
}