@od-oneapp/storage 2026.1.1301

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (69) hide show
  1. package/README.md +854 -0
  2. package/dist/client-next.d.mts +61 -0
  3. package/dist/client-next.d.mts.map +1 -0
  4. package/dist/client-next.mjs +111 -0
  5. package/dist/client-next.mjs.map +1 -0
  6. package/dist/client-utils-Dx6W25iz.d.mts +43 -0
  7. package/dist/client-utils-Dx6W25iz.d.mts.map +1 -0
  8. package/dist/client.d.mts +28 -0
  9. package/dist/client.d.mts.map +1 -0
  10. package/dist/client.mjs +183 -0
  11. package/dist/client.mjs.map +1 -0
  12. package/dist/env-BVHLmQdh.mjs +128 -0
  13. package/dist/env-BVHLmQdh.mjs.map +1 -0
  14. package/dist/env.mjs +3 -0
  15. package/dist/health-check-D7LnnDec.mjs +746 -0
  16. package/dist/health-check-D7LnnDec.mjs.map +1 -0
  17. package/dist/health-check-im_huJ59.d.mts +116 -0
  18. package/dist/health-check-im_huJ59.d.mts.map +1 -0
  19. package/dist/index.d.mts +60 -0
  20. package/dist/index.d.mts.map +1 -0
  21. package/dist/index.mjs +3 -0
  22. package/dist/keys.d.mts +37 -0
  23. package/dist/keys.d.mts.map +1 -0
  24. package/dist/keys.mjs +253 -0
  25. package/dist/keys.mjs.map +1 -0
  26. package/dist/server-edge.d.mts +28 -0
  27. package/dist/server-edge.d.mts.map +1 -0
  28. package/dist/server-edge.mjs +88 -0
  29. package/dist/server-edge.mjs.map +1 -0
  30. package/dist/server-next.d.mts +183 -0
  31. package/dist/server-next.d.mts.map +1 -0
  32. package/dist/server-next.mjs +1353 -0
  33. package/dist/server-next.mjs.map +1 -0
  34. package/dist/server.d.mts +70 -0
  35. package/dist/server.d.mts.map +1 -0
  36. package/dist/server.mjs +384 -0
  37. package/dist/server.mjs.map +1 -0
  38. package/dist/types.d.mts +321 -0
  39. package/dist/types.d.mts.map +1 -0
  40. package/dist/types.mjs +3 -0
  41. package/dist/validation.d.mts +101 -0
  42. package/dist/validation.d.mts.map +1 -0
  43. package/dist/validation.mjs +590 -0
  44. package/dist/validation.mjs.map +1 -0
  45. package/dist/vercel-blob-07Sx0Akn.d.mts +31 -0
  46. package/dist/vercel-blob-07Sx0Akn.d.mts.map +1 -0
  47. package/dist/vercel-blob-DA8HaYuw.mjs +158 -0
  48. package/dist/vercel-blob-DA8HaYuw.mjs.map +1 -0
  49. package/package.json +111 -0
  50. package/src/actions/blob-upload.ts +171 -0
  51. package/src/actions/index.ts +23 -0
  52. package/src/actions/mediaActions.ts +1071 -0
  53. package/src/actions/productMediaActions.ts +538 -0
  54. package/src/auth-helpers.ts +386 -0
  55. package/src/capabilities.ts +225 -0
  56. package/src/client-next.ts +184 -0
  57. package/src/client-utils.ts +292 -0
  58. package/src/client.ts +102 -0
  59. package/src/constants.ts +88 -0
  60. package/src/health-check.ts +81 -0
  61. package/src/multi-storage.ts +230 -0
  62. package/src/multipart.ts +497 -0
  63. package/src/retry-utils.test.ts +118 -0
  64. package/src/retry-utils.ts +59 -0
  65. package/src/server-edge.ts +129 -0
  66. package/src/server-next.ts +14 -0
  67. package/src/server.ts +666 -0
  68. package/src/validation.test.ts +312 -0
  69. package/src/validation.ts +827 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-next.mjs","names":[],"sources":["../src/auth-helpers.ts","../src/actions/mediaActions.ts","../src/actions/productMediaActions.ts","../src/actions/blob-upload.ts"],"sourcesContent":["/**\n * @fileoverview Authentication and Authorization Utilities for Storage Actions\n *\n * This module provides authentication and authorization helpers for storage server actions.\n * These functions should be implemented by the consuming application to integrate with\n * the authentication system.\n *\n * NOTE: These are placeholder functions that should be replaced with actual implementations\n * from your authentication package (e.g., @od-oneapp/auth, NextAuth, etc.)\n *\n * @module @od-oneapp/storage/auth-helpers\n */\n\nimport { logError, logWarn } from '@repo/shared/logs';\n\nimport { safeEnv } from '../env';\n\n/**\n * Retrieves the current authenticated user session\n *\n * This is a placeholder implementation that should be replaced with your actual\n * authentication system integration (NextAuth, @od-oneapp/auth, Clerk, etc.).\n *\n * **Implementation Pattern:**\n * ```typescript\n * // Example with @od-oneapp/auth:\n * const { auth } = await import('@od-oneapp/auth/server');\n * return await auth();\n *\n * // Example with NextAuth:\n * const { getServerSession } = await import('next-auth');\n * return await getServerSession(authOptions);\n * ```\n *\n * @returns Session object containing user info, or `null` if unauthenticated\n *\n * @example\n * ```typescript\n * export async function uploadMediaAction(key: string, data: Blob) {\n * 'use server';\n *\n * const session = await getSession();\n * if (!session?.user) {\n * return { success: false, error: 'Unauthorized' };\n * }\n *\n * // Proceed with authenticated upload\n * const storage = getStorage();\n * const result = await storage.upload(key, data);\n *\n * return { success: true, data: result };\n * }\n * ```\n */\nexport async function getSession(): Promise<{ user: { id: string; email?: string } } | null> {\n // Permissive-by-default: return null (unauthenticated)\n // Consuming actions should enforce auth based on env flags\n try {\n // If a real auth implementation is available, prefer it (optional dynamic import pattern)\n // const { auth } = await import('@od-oneapp/auth/server');\n // return await auth();\n return null;\n } catch {\n return null;\n }\n}\n\n/**\n * Checks if a user has permission to manage a specific product\n *\n * This is a placeholder that returns `false` by default (deny-by-default security).\n * Replace with your actual authorization logic that checks:\n * - Product ownership\n * - Role-based permissions (admin, editor, viewer)\n * - Team/organization membership\n *\n * **Implementation Pattern:**\n * ```typescript\n * // Example with database check:\n * const product = await db.product.findUnique({\n * where: { id: productId },\n * include: { team: { members: true } }\n * });\n *\n * return product?.ownerId === userId ||\n * product?.team?.members.some(m => m.userId === userId && m.role === 'admin');\n * ```\n *\n * @param userId - User ID to authorize\n * @param productId - Product ID to check access for\n * @returns `true` if user can manage the product, `false` otherwise\n *\n * @example\n * ```typescript\n * export async function deleteProductMediaAction(mediaId: string) {\n * 'use server';\n *\n * const session = await getSession();\n * if (!session?.user) {\n * return { success: false, error: 'Unauthorized' };\n * }\n *\n * const media = await db.productMedia.findUnique({ where: { id: mediaId } });\n * if (!media) {\n * return { success: false, error: 'Not found' };\n * }\n *\n * const canManage = await canUserManageProduct(session.user.id, media.productId);\n * if (!canManage) {\n * return { success: false, error: 'Forbidden' };\n * }\n *\n * await getStorage().delete(media.storageKey);\n * return { success: true };\n * }\n * ```\n */\nexport async function canUserManageProduct(_userId: string, _productId: string): Promise<boolean> {\n // Deny-by-default (restrictive): returns false until replaced with actual authorization logic\n // Replace with actual authorization logic in the consuming app\n return false;\n}\n\n/**\n * Extracts client IP address or identifier for rate limiting\n *\n * This placeholder returns `'unknown'`. Implement proper IP extraction based on:\n * - Proxy headers (X-Forwarded-For, X-Real-IP, CF-Connecting-IP)\n * - Direct connection IP\n * - User ID for authenticated rate limiting\n *\n * **Implementation Pattern:**\n * ```typescript\n * export function getClientIP(_request?: Request): string {\n * if (!request) return 'unknown';\n *\n * // Cloudflare\n * const cfIP = request.headers.get('cf-connecting-ip');\n * if (cfIP) return cfIP;\n *\n * // Behind proxy\n * const forwarded = request.headers.get('x-forwarded-for');\n * if (forwarded) return forwarded.split(',')[0]?.trim() || 'unknown';\n *\n * // Direct connection\n * const realIP = request.headers.get('x-real-ip');\n * return realIP || 'unknown';\n * }\n * ```\n *\n * @param request - Optional Request object to extract IP from\n * @returns Client IP address or `'unknown'` if not determinable\n *\n * @example\n * ```typescript\n * export async function uploadAction(request: Request, data: FormData) {\n * 'use server';\n *\n * const clientIP = getClientIP(request);\n * const rateLimit = await checkRateLimit(\n * clientIP,\n * 'storage:upload',\n * 100, // max requests\n * 60000 // per minute\n * );\n *\n * if (!rateLimit.allowed) {\n * return {\n * success: false,\n * error: 'Rate limit exceeded',\n * resetAt: rateLimit.resetAt\n * };\n * }\n *\n * // Process upload...\n * }\n * ```\n */\nexport function getClientIP(_request?: Request): string {\n // TODO: Implement IP extraction from request headers\n // Example:\n // if (request) {\n // return request.headers.get('x-forwarded-for')?.split(',')[0] ||\n // request.headers.get('x-real-ip') ||\n // 'unknown';\n // }\n\n // Fallback for server actions (may need to pass request through)\n return 'unknown';\n}\n\n/**\n * Checks if an identifier has exceeded rate limits for a storage action\n *\n * This placeholder always allows requests when `STORAGE_ENABLE_RATE_LIMIT=false`.\n * When enabled, implement with Redis, Upstash, or in-memory store.\n *\n * **Implementation Pattern (Redis):**\n * ```typescript\n * import { Redis } from '@upstash/redis';\n *\n * export async function checkRateLimit(\n * identifier: string,\n * action: string,\n * maxRequests: number,\n * windowMs: number\n * ) {\n * const redis = Redis.fromEnv();\n * const key = `ratelimit:${action}:${identifier}`;\n *\n * const count = await redis.incr(key);\n * if (count === 1) {\n * await redis.pexpire(key, windowMs);\n * }\n *\n * const ttl = await redis.pttl(key);\n * const resetAt = new Date(Date.now() + ttl);\n *\n * return {\n * allowed: count <= maxRequests,\n * remaining: Math.max(0, maxRequests - count),\n * resetAt\n * };\n * }\n * ```\n *\n * @param identifier - Unique identifier (IP address, user ID, API key)\n * @param action - Action key for rate limiting (e.g., `'storage:upload'`, `'storage:delete'`)\n * @param maxRequests - Maximum requests allowed in the time window\n * @param windowMs - Time window in milliseconds (e.g., 60000 for 1 minute)\n * @returns Rate limit status with allowed flag, remaining count, and reset time\n *\n * @example\n * ```typescript\n * export async function uploadMediaAction(key: string, data: Blob) {\n * 'use server';\n *\n * const session = await getSession();\n * const identifier = session?.user.id || getClientIP();\n *\n * const limit = await checkRateLimit(identifier, 'storage:upload', 100, 60000);\n * if (!limit.allowed) {\n * return {\n * success: false,\n * error: `Rate limit exceeded. ${limit.remaining} requests remaining. Resets at ${limit.resetAt.toISOString()}`,\n * };\n * }\n *\n * const storage = getStorage();\n * const result = await storage.upload(key, data);\n * return { success: true, data: result };\n * }\n * ```\n */\nexport async function checkRateLimit(\n identifier: string,\n action: string,\n maxRequests: number,\n windowMs: number,\n): Promise<{ allowed: boolean; remaining: number; resetAt: Date }> {\n // Check feature flag to decide behavior\n const { safeEnv } = await import('../env');\n const env = safeEnv();\n\n if (!env.STORAGE_ENABLE_RATE_LIMIT) {\n // Permissive: allow\n return {\n allowed: true,\n remaining: maxRequests,\n resetAt: new Date(Date.now() + windowMs),\n };\n }\n\n // If enabled but no implementation, allow but with zero remaining to encourage integration\n return {\n allowed: true,\n remaining: Math.max(0, maxRequests - 1),\n resetAt: new Date(Date.now() + windowMs),\n };\n}\n\n/**\n * Validate a CSRF token according to runtime enforcement settings.\n *\n * If the environment flag `STORAGE_ENFORCE_CSRF` is false or the environment is unavailable, validation is permissive and this function returns `true`. When enforcement is enabled, the function returns `true` only if `token` is a non-empty string.\n *\n * @param token - CSRF token to validate\n * @param request - Optional request object that consuming applications may inspect when implementing real validation\n * @returns `true` if the token is considered valid (or enforcement is disabled / env unavailable), `false` otherwise\n * @deprecated Use validateCSRFOrigin() for better origin-based validation\n */\nexport function validateCSRFToken(token: string, _request?: Request): boolean {\n // Enforce behavior based on env flag\n try {\n const env = safeEnv();\n if (!env.STORAGE_ENFORCE_CSRF) {\n // Permissive-by-default: skip CSRF enforcement\n return true;\n }\n } catch {\n // If env not available, be permissive in package context\n return true;\n }\n\n // If enforcement is enabled but no real validation is available, fail closed\n return typeof token === 'string' && token.length > 0;\n}\n\n/**\n * Validates CSRF protection by checking request origin matches host\n *\n * Next.js has built-in origin checking for Server Actions, but this adds\n * explicit validation for defense-in-depth security.\n *\n * This function validates:\n * - The request origin header matches the host header\n * - Optional x-csrf-token header if provided\n *\n * @returns `true` if CSRF validation passes or is disabled, `false` if validation fails\n *\n * @example\n * ```typescript\n * export async function uploadMediaAction(...) {\n * 'use server';\n *\n * const csrfValid = await validateCSRFOrigin();\n * if (!csrfValid) {\n * return { success: false, error: 'Invalid request origin' };\n * }\n * // ... proceed with action\n * }\n * ```\n */\nexport async function validateCSRFOrigin(): Promise<boolean> {\n const env = safeEnv();\n\n if (!env.STORAGE_ENFORCE_CSRF) {\n return true; // CSRF validation disabled\n }\n\n try {\n const { headers } = await import('next/headers');\n const headersList = await headers();\n const origin = headersList.get('origin');\n const host = headersList.get('host');\n\n // Validate origin matches host\n if (origin && host) {\n try {\n const originUrl = new URL(origin);\n const expectedOrigin = `${originUrl.protocol}//${host}`;\n\n if (origin !== expectedOrigin) {\n logWarn('CSRF validation failed: origin mismatch', {\n origin,\n expectedOrigin,\n host,\n });\n return false;\n }\n } catch (error) {\n logError('CSRF validation error: invalid origin URL', {\n origin,\n error,\n });\n return false;\n }\n }\n\n // Additional: Validate custom CSRF token if provided\n const csrfToken = headersList.get('x-csrf-token');\n if (csrfToken) {\n // Basic validation - consuming apps should enhance this with session validation\n // For now, just check it's a non-empty string\n if (typeof csrfToken !== 'string' || csrfToken.length === 0) {\n return false;\n }\n }\n\n return true;\n } catch (error) {\n // If headers() fails (not in Next.js context), be permissive for package usage\n logWarn('CSRF validation skipped: not in Next.js context', { error });\n return true;\n }\n}\n","/**\n * @fileoverview Media storage server actions\n *\n * Provides Next.js server actions for media file operations including upload,\n * download, delete, move, and list operations with authentication and validation.\n *\n * Features:\n * - Authentication and authorization\n * - Rate limiting\n * - File validation (size, MIME type)\n * - SSRF protection\n * - Bulk operations\n *\n * @module @repo/storage/actions/mediaActions\n */\n\n'use server';\n\nimport { safeEnv } from '../../env';\nimport { validateStorageKey } from '../../keys';\nimport { checkRateLimit, getClientIP, getSession } from '../auth-helpers';\nimport { STORAGE_CONSTANTS } from '../constants';\nimport { getMultiStorage, getStorage } from '../server';\nimport { validateMimeType, validateUrlForSSRF } from '../validation';\n\nimport type {\n BulkDeleteResponse,\n BulkMoveResponse,\n ListOptions,\n MediaActionResponse,\n StorageObject,\n UploadOptions,\n} from '../../types';\n\n//==============================================================================\n// MEDIA STORAGE SERVER ACTIONS\n//==============================================================================\n\n/**\n * Upload binary data to the configured storage provider with optional validations.\n *\n * Validations performed before upload include storage key safety checks, optional\n * authentication and rate limiting, maximum file size enforcement, and MIME type\n * validation when `options.allowedMimeTypes` and `options.contentType` are provided.\n *\n * @param key - Destination storage key (path) where the file will be stored\n * @param data - File contents to upload (Buffer, ArrayBuffer, Blob, File, or ReadableStream)\n * @param options - Upload options and validation overrides. Notable fields:\n * - `maxFileSize` — override maximum allowed file size in bytes for this upload\n * - `allowedMimeTypes` — list of permitted MIME types; validated against `options.contentType` if present\n * - other provider-specific upload options (e.g., `contentType`, `metadata`) are passed through to the storage provider\n * @returns An object with `success: true` and the uploaded `StorageObject` on success, or `success: false` and an `error` message on failure\n */\nexport async function uploadMediaAction(\n key: string,\n data: ArrayBuffer | Blob | Buffer | File | ReadableStream,\n options?: UploadOptions & {\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n },\n): Promise<MediaActionResponse<StorageObject>> {\n 'use server';\n\n // Get environment configuration once\n const storageEnv = safeEnv();\n\n // Authentication check (configurable)\n const session = await getSession();\n if (storageEnv.STORAGE_ENFORCE_AUTH && !session?.user) {\n return {\n success: false,\n error: 'Unauthorized: Authentication required',\n };\n }\n\n // Rate limiting\n const identifier = session?.user?.id ?? getClientIP();\n const rateLimitResult = await checkRateLimit(\n identifier,\n 'storage:upload',\n storageEnv.STORAGE_RATE_LIMIT_REQUESTS,\n storageEnv.STORAGE_RATE_LIMIT_WINDOW_MS,\n );\n if (storageEnv.STORAGE_ENABLE_RATE_LIMIT && !rateLimitResult.allowed) {\n return {\n success: false,\n error: 'Rate limit exceeded. Please try again later.',\n };\n }\n\n try {\n // Validate storage key to prevent path traversal\n const keyValidation = validateStorageKey(key, {\n maxLength: 1024,\n forbiddenPatterns: [/\\.\\./, /\\/\\//],\n });\n if (!keyValidation.valid) {\n return {\n success: false,\n error: `Invalid storage key: ${keyValidation.errors.join(', ')}`,\n };\n }\n\n // Validate file size\n const maxSize = options?.maxFileSize ?? storageEnv.STORAGE_MAX_FILE_SIZE;\n let fileSize = 0;\n\n if (data instanceof File || data instanceof Blob) {\n fileSize = data.size;\n } else if (data instanceof ArrayBuffer) {\n fileSize = data.byteLength;\n } else if (data instanceof Buffer) {\n fileSize = data.length;\n }\n\n if (fileSize > maxSize) {\n return {\n success: false,\n error: `File size ${fileSize} bytes exceeds maximum ${maxSize} bytes`,\n };\n }\n\n // Validate MIME type if restrictions provided\n if (options?.allowedMimeTypes && options.contentType) {\n const mimeValidation = validateMimeType(options.contentType, options.allowedMimeTypes);\n if (!mimeValidation.valid) {\n return {\n success: false,\n error: mimeValidation.error ?? 'Invalid file type',\n };\n }\n }\n\n const result = await getStorage().upload(key, data, options);\n return { success: true, data: result };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to upload media',\n };\n }\n}\n\n/**\n * Get media file metadata\n */\nexport async function getMediaAction(key: string): Promise<MediaActionResponse<StorageObject>> {\n 'use server';\n try {\n const metadata = await getStorage().getMetadata(key);\n return { success: true, data: metadata };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to get media metadata',\n };\n }\n}\n\n/**\n * List media files\n */\nexport async function listMediaAction(\n options?: ListOptions,\n): Promise<MediaActionResponse<StorageObject[]>> {\n 'use server';\n try {\n const items = await getStorage().list(options);\n return { success: true, data: items };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list media',\n };\n }\n}\n\n/**\n * Delete a media file\n */\nexport async function deleteMediaAction(key: string): Promise<MediaActionResponse<void>> {\n 'use server';\n try {\n await getStorage().delete(key);\n return { success: true };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to delete media',\n };\n }\n}\n\n/**\n * Determine whether a media object exists at the given storage key.\n *\n * @param key - The storage key (path) of the media object to check\n * @returns A response object whose `data` is `true` if the object exists, `false` otherwise. On error the response will have `success: false` and an `error` message\n */\nexport async function existsMediaAction(key: string): Promise<MediaActionResponse<boolean>> {\n 'use server';\n try {\n const exists = await getStorage().exists(key);\n return { success: true, data: exists };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to check media existence',\n };\n }\n}\n\n/**\n * Retrieve a public or signed URL for a media object.\n *\n * When the context is `product`, when `forceSign` is true, or when `expiresIn` is provided,\n * a signed URL is returned using the specified or default expiry; otherwise a direct URL is returned.\n *\n * @param key - The storage key of the media object\n * @param options.expiresIn - Expiration time in seconds for a signed URL\n * @param options.context - Context of the media; `product` forces signed URLs for product photos\n * @param options.forceSign - When true, force generation of a signed URL regardless of context\n * @returns The URL for the storage key (signed if signing rules apply)\n */\nexport async function getMediaUrlAction(\n key: string,\n options?: {\n expiresIn?: number;\n context?: 'product' | 'user' | 'admin' | 'public';\n forceSign?: boolean;\n },\n): Promise<MediaActionResponse<string>> {\n 'use server';\n try {\n const storage = getStorage();\n\n // Product photos always need signed URLs for protection\n const isProductPhoto = options?.context === 'product' || key.includes('/products/');\n const shouldSign = (isProductPhoto || options?.forceSign) ?? Boolean(options?.expiresIn);\n\n if (shouldSign) {\n const expiresIn =\n options?.expiresIn ??\n (isProductPhoto\n ? STORAGE_CONSTANTS.PRODUCT_URL_EXPIRY_SECONDS\n : STORAGE_CONSTANTS.UPLOAD_URL_EXPIRY_SECONDS);\n const signedUrl = await storage.getUrl(key, { expiresIn });\n return { success: true, data: signedUrl };\n }\n\n // For public content, return direct URL\n const url = await storage.getUrl(key);\n return { success: true, data: url };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to get media URL',\n };\n }\n}\n\n/**\n * Generate signed URLs for multiple product media keys.\n *\n * Appends the provided `variant` to keys that reference Cloudflare Images and uses\n * `options.expiresIn` or the product URL expiry constant as the signing duration.\n *\n * @param keys - Array of storage keys identifying product media items\n * @param options - Optional settings: `expiresIn` (seconds) overrides the default signed URL expiry; `variant` is appended to Cloudflare Images keys to request a specific image variant\n * @returns A MediaActionResponse whose `data` is an array of objects with the original `key` and the generated `url` when successful; on failure the response contains an error message\n */\nexport async function getProductMediaUrlsAction(\n keys: string[],\n options?: { expiresIn?: number; variant?: string },\n): Promise<MediaActionResponse<Array<{ key: string; url: string }>>> {\n 'use server';\n try {\n const storage = getStorage();\n const expiresIn = options?.expiresIn ?? STORAGE_CONSTANTS.PRODUCT_URL_EXPIRY_SECONDS;\n\n const mediaWithSignedUrls = await Promise.all(\n keys.map(async key => {\n let finalKey = key;\n\n // For Cloudflare Images, append variant if specified\n if (options?.variant && key.includes('cloudflare-images')) {\n finalKey = `${key}/${options.variant}`;\n }\n\n const signedUrl = await storage.getUrl(finalKey, { expiresIn });\n\n return {\n key,\n url: signedUrl,\n };\n }),\n );\n\n return {\n success: true,\n data: mediaWithSignedUrls,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to get product media URLs',\n };\n }\n}\n\n/**\n * Get presigned upload URL for product photos (admin only)\n */\nexport async function getProductUploadUrlAction(\n filename: string,\n productId: string,\n options?: {\n expiresIn?: number;\n contentType?: string;\n maxSizeBytes?: number;\n },\n): Promise<MediaActionResponse<{ uploadUrl: string; key: string }>> {\n 'use server';\n try {\n const storage = getStorage();\n const key = `products/${productId}/${Date.now()}-${filename}`;\n\n // Get presigned upload URL (this would depend on the storage provider)\n // For now, return a structure that indicates what should be implemented\n const uploadUrl = await storage.getUrl(key, {\n expiresIn: options?.expiresIn ?? 1800, // 30 minutes for uploads\n });\n\n return {\n success: true,\n data: {\n uploadUrl,\n key,\n },\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to get product upload URL',\n };\n }\n}\n\n/**\n * Download a media file\n */\nexport async function downloadMediaAction(key: string): Promise<MediaActionResponse<Blob>> {\n 'use server';\n try {\n const blob = await getStorage().download(key);\n return { success: true, data: blob };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to download media',\n };\n }\n}\n\n//==============================================================================\n// BULK OPERATIONS\n//==============================================================================\n\n/**\n * Delete multiple media files\n */\nexport async function bulkDeleteMediaAction(\n keys: string[],\n): Promise<MediaActionResponse<BulkDeleteResponse>> {\n 'use server';\n const results = {\n succeeded: [] as string[],\n failed: [] as { key: string; error: string }[],\n };\n\n try {\n // Process deletions in parallel with error handling for each\n await Promise.all(\n keys.map(async key => {\n try {\n await getStorage().delete(key);\n results.succeeded.push(key);\n } catch (error) {\n results.failed.push({\n key,\n error: error instanceof Error ? error.message : 'Unknown error',\n });\n }\n }),\n );\n\n return {\n success: results.failed.length === 0,\n data: results,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Bulk delete operation failed',\n };\n }\n}\n\n/**\n * Move/rename multiple media files\n */\nexport async function bulkMoveMediaAction(\n operations: Array<{ sourceKey: string; destinationKey: string }>,\n): Promise<MediaActionResponse<BulkMoveResponse>> {\n 'use server';\n const results = {\n succeeded: [] as { sourceKey: string; destinationKey: string }[],\n failed: [] as { sourceKey: string; destinationKey: string; error: string }[],\n };\n\n try {\n // Process moves in parallel\n await Promise.all(\n operations.map(async ({ sourceKey, destinationKey }) => {\n try {\n // Download the file\n const blob = await getStorage().download(sourceKey);\n\n // Get metadata from source\n const metadata = await getStorage().getMetadata(sourceKey);\n\n // Upload to new location\n await getStorage().upload(destinationKey, blob, {\n contentType: metadata.contentType,\n });\n\n // Delete the source\n await getStorage().delete(sourceKey);\n\n results.succeeded.push({ sourceKey, destinationKey });\n } catch (error) {\n results.failed.push({\n sourceKey,\n destinationKey,\n error: error instanceof Error ? error.message : 'Unknown error',\n });\n }\n }),\n );\n\n return {\n success: results.failed.length === 0,\n data: results,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Bulk move operation failed',\n };\n }\n}\n\n//==============================================================================\n// BULK IMPORT OPERATIONS\n//==============================================================================\n\n/**\n * Imports multiple external URLs into storage, processing them in configurable batches.\n *\n * Each source URL is validated for safety, fetched with a per-request timeout, and streamed\n * into the chosen storage provider (or routed to Cloudflare Images for images when available).\n * Successful imports are recorded with their destination key and resulting StorageObject;\n * failures record the source URL and an error message. Processing continues after individual failures.\n *\n * @param imports - Array of import entries, each with a required `sourceUrl`, optional `destinationKey`\n * (if omitted a key is generated), and optional `metadata` (altText, productId, userId, type).\n * @param options - Optional settings:\n * - `batchSize`: number of concurrent imports per batch (defaults to STORAGE_CONSTANTS.DEFAULT_BATCH_SIZE).\n * - `provider`: name of the storage provider to use (if omitted uses default provider or Cloudflare Images for images).\n * - `timeout`: per-request fetch timeout in milliseconds (defaults to STORAGE_CONSTANTS.DEFAULT_REQUEST_TIMEOUT_MS).\n * @returns An object with:\n * - `succeeded`: array of { sourceUrl, destinationKey, storageObject } for successful imports;\n * - `failed`: array of { sourceUrl, error } for failed imports;\n * - `totalProcessed`: total number of import attempts processed.\n */\nexport async function bulkImportFromUrlsAction(\n imports: Array<{\n sourceUrl: string;\n destinationKey?: string;\n metadata?: {\n altText?: string;\n productId?: string;\n userId?: string;\n type?: 'IMAGE' | 'VIDEO' | 'DOCUMENT';\n };\n }>,\n options?: {\n batchSize?: number;\n provider?: string;\n timeout?: number;\n },\n): Promise<\n MediaActionResponse<{\n succeeded: Array<{\n sourceUrl: string;\n destinationKey: string;\n storageObject: StorageObject;\n }>;\n failed: Array<{\n sourceUrl: string;\n error: string;\n }>;\n totalProcessed: number;\n }>\n> {\n 'use server';\n\n const results = {\n succeeded: [] as Array<{\n sourceUrl: string;\n destinationKey: string;\n storageObject: StorageObject;\n }>,\n failed: [] as Array<{\n sourceUrl: string;\n error: string;\n }>,\n totalProcessed: 0,\n };\n\n const batchSize = options?.batchSize ?? STORAGE_CONSTANTS.DEFAULT_BATCH_SIZE;\n const timeout = options?.timeout ?? STORAGE_CONSTANTS.DEFAULT_REQUEST_TIMEOUT_MS;\n\n try {\n // Process imports in batches to avoid overwhelming the system\n for (let i = 0; i < imports.length; i += batchSize) {\n const batch = imports.slice(i, i + batchSize);\n\n await Promise.all(\n batch.map(async importItem => {\n try {\n // Validate URL to prevent SSRF attacks\n const urlValidation = validateUrlForSSRF(importItem.sourceUrl);\n if (!urlValidation.valid) {\n throw new Error(urlValidation.error ?? 'Invalid or unsafe URL');\n }\n\n // Fetch with timeout\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeout);\n\n const response = await fetch(importItem.sourceUrl, {\n signal: controller.signal,\n // Add additional security headers\n headers: {\n 'User-Agent': 'Storage-Service/1.0',\n },\n // Prevent redirects to internal URLs\n redirect: 'error',\n });\n\n clearTimeout(timeoutId);\n\n if (!response.ok) {\n throw new Error(`HTTP ${response.status}: ${response.statusText}`);\n }\n\n // Get content type and determine storage provider\n const contentType = response.headers.get('content-type') ?? 'application/octet-stream';\n const isImage = contentType.startsWith('image/');\n\n // Generate destination key if not provided\n const destinationKey =\n importItem.destinationKey ??\n generateStorageKey(importItem.sourceUrl, importItem.metadata);\n\n // Stream the content to storage\n const storage = options?.provider\n ? getMultiStorage().getProvider(options.provider)\n : getStorage();\n\n if (!storage) {\n throw new Error('Storage provider not available');\n }\n\n // For images, use Cloudflare Images if configured\n if (isImage && !options?.provider) {\n const multiStorage = getMultiStorage();\n const imageProvider = multiStorage.getProvider('cloudflare-images');\n if (imageProvider) {\n // Stream to Cloudflare Images\n const blob = await response.blob();\n const result = await imageProvider.upload(destinationKey, blob, {\n contentType,\n metadata: importItem.metadata,\n });\n\n results.succeeded.push({\n sourceUrl: importItem.sourceUrl,\n destinationKey,\n storageObject: result,\n });\n results.totalProcessed++;\n return;\n }\n }\n\n // Stream to default storage (R2 or local)\n const stream = response.body;\n if (!stream) {\n throw new Error('No response body available');\n }\n\n const result = await storage.upload(destinationKey, stream, {\n contentType,\n metadata: importItem.metadata,\n });\n\n results.succeeded.push({\n sourceUrl: importItem.sourceUrl,\n destinationKey,\n storageObject: result,\n });\n results.totalProcessed++;\n } catch (error) {\n results.failed.push({\n sourceUrl: importItem.sourceUrl,\n error: error instanceof Error ? error.message : 'Unknown error',\n });\n results.totalProcessed++;\n }\n }),\n );\n }\n\n return {\n success: results.failed.length === 0,\n data: results,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Bulk import operation failed',\n data: results,\n };\n }\n}\n\n/**\n * Import a single media item from a remote HTTPS URL into storage.\n *\n * Validates the source URL for safety, fetches the resource, and uploads it to storage.\n * If `options.onProgress` is provided and the response exposes a `content-length`, progress\n * updates are emitted as a fraction between 0 and 1 during download.\n *\n * @param sourceUrl - The HTTPS URL of the resource to import; must pass SSRF-safe validation.\n * @param destinationKey - Optional destination storage key; generated automatically when omitted.\n * @param options.metadata - Optional metadata to attach to the uploaded object.\n * @param options.onProgress - Optional callback invoked with a number in [0, 1] representing download progress.\n * @returns On success, a response containing the uploaded `StorageObject`; on failure, a response containing an error message.\n */\nexport async function importFromUrlAction(\n sourceUrl: string,\n destinationKey?: string,\n options?: {\n metadata?: Record<string, any>;\n onProgress?: (progress: number) => void;\n },\n): Promise<MediaActionResponse<StorageObject>> {\n 'use server';\n\n try {\n // Validate URL to prevent SSRF attacks\n const urlValidation = validateUrlForSSRF(sourceUrl);\n if (!urlValidation.valid) {\n throw new Error(urlValidation.error ?? 'Invalid or unsafe URL');\n }\n\n const response = await fetch(sourceUrl, {\n // Add additional security headers\n headers: {\n 'User-Agent': 'Storage-Service/1.0',\n },\n // Prevent redirects to internal URLs\n redirect: 'error',\n });\n\n if (!response.ok) {\n throw new Error(`Failed to fetch: ${response.status} ${response.statusText}`);\n }\n\n const contentType = response.headers.get('content-type') ?? 'application/octet-stream';\n const contentLength = response.headers.get('content-length');\n\n // Generate key if not provided\n const key = destinationKey ?? generateStorageKey(sourceUrl);\n\n // For progress tracking, we need to read the stream manually\n if (options?.onProgress && contentLength && response.body) {\n const total = parseInt(contentLength);\n let loaded = 0;\n\n const reader = response.body.getReader();\n const chunks: Uint8Array[] = [];\n\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n\n chunks.push(value);\n loaded += value.length;\n options.onProgress(loaded / total);\n }\n\n // Combine chunks into a single blob\n const blob = new Blob(\n chunks.map(\n chunk =>\n chunk.buffer.slice(\n chunk.byteOffset,\n chunk.byteOffset + chunk.byteLength,\n ) as ArrayBuffer,\n ),\n { type: contentType },\n );\n\n const storage = getStorage();\n const result = await storage.upload(key, blob, {\n contentType,\n metadata: options.metadata,\n });\n\n return { success: true, data: result };\n } else {\n // Simple stream without progress\n const storage = getStorage();\n const stream = response.body;\n\n if (!stream) {\n throw new Error('No response body available');\n }\n\n const result = await storage.upload(key, stream, {\n contentType,\n metadata: options?.metadata,\n });\n\n return { success: true, data: result };\n }\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to import from URL',\n };\n }\n}\n\n/**\n * Helper function to generate storage key from URL\n */\nfunction generateStorageKey(sourceUrl: string, metadata?: Record<string, any>): string {\n try {\n const url = new URL(sourceUrl);\n const filename = url.pathname.split('/').pop() ?? 'imported-file';\n const timestamp = Date.now();\n\n // Determine prefix based on metadata\n let prefix = 'imports';\n if (metadata?.productId) {\n prefix = `products/${metadata.productId}`;\n } else if (metadata?.userId) {\n prefix = `users/${metadata.userId}`;\n } else if (metadata?.type === 'IMAGE') {\n prefix = 'images';\n } else if (metadata?.type === 'VIDEO') {\n prefix = 'videos';\n } else if (metadata?.type === 'DOCUMENT') {\n prefix = 'documents';\n }\n\n return `${prefix}/${timestamp}-${filename}`;\n } catch {\n // Fallback for invalid URLs\n return `imports/${Date.now()}-imported-file`;\n }\n}\n\n//==============================================================================\n// MULTI-STORAGE OPERATIONS\n//==============================================================================\n\n/**\n * Upload to a specific storage provider\n */\nexport async function uploadToProviderAction(\n providerName: string,\n key: string,\n data: ArrayBuffer | Blob | Buffer | File | ReadableStream,\n options?: UploadOptions,\n): Promise<MediaActionResponse<StorageObject>> {\n 'use server';\n try {\n const provider = getMultiStorage().getProvider(providerName);\n if (!provider) {\n throw new Error(`Provider '${providerName}' not found`);\n }\n const result = await provider.upload(key, data, options);\n return { success: true, data: result };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to upload to provider',\n };\n }\n}\n\n/**\n * List available storage providers\n */\nexport async function listProvidersAction(): Promise<MediaActionResponse<string[]>> {\n 'use server';\n try {\n const providers = getMultiStorage().getProviderNames();\n return { success: true, data: providers };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to list providers',\n };\n }\n}\n\n/**\n * Copy media between providers\n */\nexport async function copyBetweenProvidersAction(\n sourceProvider: string,\n destinationProvider: string,\n key: string,\n options?: UploadOptions,\n): Promise<MediaActionResponse<StorageObject>> {\n 'use server';\n try {\n const multiStorage = getMultiStorage();\n const source = multiStorage.getProvider(sourceProvider);\n const destination = multiStorage.getProvider(destinationProvider);\n\n if (!source) {\n throw new Error(`Source provider '${sourceProvider}' not found`);\n }\n if (!destination) {\n throw new Error(`Destination provider '${destinationProvider}' not found`);\n }\n\n // Download from source\n const blob = await source.download(key);\n const metadata = await source.getMetadata(key);\n\n // Upload to destination\n const result = await destination.upload(key, blob, {\n contentType: metadata.contentType,\n ...options,\n });\n\n return { success: true, data: result };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to copy between providers',\n };\n }\n}\n\n/**\n * Create an empty folder (placeholder with trailing slash)\n */\nexport async function createFolderAction(\n key: string,\n options?: UploadOptions,\n): Promise<MediaActionResponse<StorageObject>> {\n 'use server';\n try {\n // Ensure key ends with slash for folder\n const folderKey = key.endsWith('/') ? key : `${key}/`;\n\n // Create empty folder by uploading empty content\n const emptyContent = Buffer.from(new Uint8Array(0));\n const result = await getStorage().upload(folderKey, emptyContent, {\n contentType: 'application/x-directory',\n ...options,\n });\n\n return { success: true, data: result };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to create folder',\n };\n }\n}\n\n/**\n * Copy media between storage locations\n */\nexport async function copyMediaAction(\n sourceKey: string,\n destinationKey: string,\n options?: UploadOptions,\n): Promise<MediaActionResponse<StorageObject>> {\n 'use server';\n try {\n // Download from source\n const blob = await getStorage().download(sourceKey);\n\n // Upload to destination\n const result = await getStorage().upload(destinationKey, blob, {\n contentType: options?.contentType,\n ...options,\n });\n\n return { success: true, data: result };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to copy media',\n };\n }\n}\n\n/**\n * Get presigned upload URL for direct client uploads\n */\nexport async function getPresignedUploadUrlAction(\n key: string,\n options?: { expiresIn?: number; contentType?: string },\n): Promise<MediaActionResponse<{ url: string; fields: Record<string, string>; expiresAt: Date }>> {\n 'use server';\n try {\n const provider = getStorage();\n\n if (!provider.getPresignedUploadUrl) {\n return {\n success: false,\n error: 'Provider does not support presigned URLs',\n };\n }\n\n const result = await provider.getPresignedUploadUrl(key, options);\n\n return { success: true, data: result };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to get presigned upload URL',\n };\n }\n}\n\n/**\n * Get storage provider capabilities\n */\nexport async function getStorageCapabilitiesAction(): Promise<\n MediaActionResponse<{\n multipart: boolean;\n presignedUrls: boolean;\n progressTracking: boolean;\n abortSignal: boolean;\n metadata: boolean;\n customDomains: boolean;\n edgeCompatible: boolean;\n }>\n> {\n 'use server';\n try {\n const provider = getStorage();\n const capabilities = provider.getCapabilities?.() ?? {\n multipart: false,\n presignedUrls: false,\n progressTracking: false,\n abortSignal: false,\n metadata: false,\n customDomains: false,\n edgeCompatible: false,\n };\n\n return { success: true, data: capabilities };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to get storage capabilities',\n };\n }\n}\n\n/**\n * Validate a file's size, MIME type, and extension against provided constraints.\n *\n * Checks the file's byte size against `maxFileSize`, verifies the MIME type against\n * `allowedMimeTypes` (supports wildcard patterns like `image/*`), and verifies the\n * filename extension against `allowedExtensions`.\n *\n * @param file - The file to validate; must include `size`, `type`, and `name`.\n * @param options.maxFileSize - Maximum allowed file size in bytes.\n * @param options.allowedMimeTypes - Allowed MIME types; supports exact types and wildcards (e.g., `image/*`).\n * @param options.allowedExtensions - Allowed file extensions including the leading dot (e.g., `.jpg`, `.png`).\n * @returns An object with `valid` set to `true` when no validation errors were found, and `errors` listing any validation messages.\n */\nexport async function validateFileAction(\n file: { size: number; type: string; name: string },\n options?: {\n maxFileSize?: number;\n allowedMimeTypes?: string[];\n allowedExtensions?: string[];\n },\n): Promise<MediaActionResponse<{ valid: boolean; errors: string[] }>> {\n 'use server';\n try {\n const errors: string[] = [];\n\n // Validate file size\n if (options?.maxFileSize && file.size > options.maxFileSize) {\n errors.push(`File size ${file.size} bytes exceeds maximum ${options.maxFileSize} bytes`);\n }\n\n // Validate MIME type\n if (options?.allowedMimeTypes && options.allowedMimeTypes.length > 0) {\n const normalizedMimeType = file.type.toLowerCase().trim();\n const normalizedAllowed = options.allowedMimeTypes.map(t => t.toLowerCase().trim());\n\n if (!normalizedAllowed.includes(normalizedMimeType)) {\n // Check wildcard patterns\n const wildcardMatch = normalizedAllowed.some(allowed => {\n if (allowed.endsWith('/*')) {\n const prefix = allowed.slice(0, -2);\n return normalizedMimeType.startsWith(prefix);\n }\n return false;\n });\n\n if (!wildcardMatch) {\n errors.push(\n `MIME type '${file.type}' is not allowed. Allowed types: ${options.allowedMimeTypes.join(', ')}`,\n );\n }\n }\n }\n\n // Validate file extension\n if (options?.allowedExtensions && options.allowedExtensions.length > 0) {\n const extension = file.name.match(/\\.[^/.]+$/)?.[0]?.toLowerCase();\n if (extension && !options.allowedExtensions.includes(extension)) {\n errors.push(\n `File extension ${extension} is not allowed. Allowed: ${options.allowedExtensions.join(', ')}`,\n );\n }\n }\n\n return {\n success: true,\n data: {\n valid: errors.length === 0,\n errors,\n },\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to validate file',\n };\n }\n}\n","/**\n * @fileoverview Product media business logic server actions\n *\n * Provides Next.js server actions specifically for product media operations.\n * Includes business logic for product image management with vendor and admin contexts.\n *\n * Features:\n * - Product-specific media uploads\n * - Vendor and admin role support\n * - Product media metadata management\n * - Authorization checks\n *\n * @module @repo/storage/actions/productMediaActions\n */\n\n'use server';\n\nimport { safeEnv } from '../../env';\nimport { sanitizeStorageKey, validateStorageKey } from '../../keys';\nimport { canUserManageProduct, checkRateLimit, getSession } from '../auth-helpers';\nimport { STORAGE_CONSTANTS } from '../constants';\nimport { getStorage } from '../server';\n\nimport type { MediaActionResponse } from '../../types';\n\n//==============================================================================\n// PRODUCT MEDIA BUSINESS LOGIC ACTIONS\n//==============================================================================\n\n/**\n * Uploads multiple media files for a product while enforcing optional authentication, authorization, and rate limits.\n *\n * @param productId - The ID of the product to attach media to.\n * @param files - Files to upload; each object must include `filename`, `contentType`, and binary `data`.\n * @param options - Optional settings: `context` is the uploader role (`admin` | `vendor`); `altText`, `description`, and `tags` supply metadata.\n * @returns The action response. On success, `data` is an array of uploaded items containing `key`, `url`, and a temporary `mediaId`; on failure, `error` describes the problem.\n */\nexport async function uploadProductMediaAction(\n productId: string,\n files: Array<{\n filename: string;\n contentType: string;\n data: ArrayBuffer | Blob | Buffer | File;\n }>,\n options?: {\n context: 'admin' | 'vendor';\n altText?: string;\n description?: string;\n tags?: string[];\n },\n): Promise<MediaActionResponse<Array<{ key: string; url: string; mediaId: string }>>> {\n 'use server';\n\n try {\n // Authentication check (configurable)\n const featureEnv = safeEnv();\n const session = await getSession();\n if (featureEnv.STORAGE_ENFORCE_AUTH && !session?.user) {\n return {\n success: false,\n error: 'Unauthorized: Authentication required',\n };\n }\n\n // Permission check for product access (only when auth enforced)\n const hasPermission = session?.user\n ? await canUserManageProduct(session.user.id, productId)\n : false;\n if (featureEnv.STORAGE_ENFORCE_AUTH && !hasPermission) {\n return {\n success: false,\n error: 'Forbidden: Insufficient permissions',\n };\n }\n\n // Rate limiting\n const env = safeEnv();\n const rateLimitResult = await checkRateLimit(\n session?.user?.id ?? 'anonymous',\n 'storage:upload',\n env.STORAGE_RATE_LIMIT_REQUESTS,\n env.STORAGE_RATE_LIMIT_WINDOW_MS,\n );\n if (env.STORAGE_ENABLE_RATE_LIMIT && !rateLimitResult.allowed) {\n return {\n success: false,\n error: 'Rate limit exceeded. Please try again later.',\n };\n }\n\n const storage = getStorage();\n const results = [];\n\n for (const [index, file] of files.entries()) {\n // Sanitize filename to prevent path traversal attacks\n const sanitizedFilename = sanitizeStorageKey(file.filename);\n\n // Generate storage key with sanitized filename\n const timestamp = Date.now();\n const key = `products/${productId}/images/${timestamp}-${index}-${sanitizedFilename}`;\n\n // Validate storage key to prevent path traversal\n const keyValidation = validateStorageKey(key, {\n maxLength: 1024,\n forbiddenPatterns: [/\\.\\./, /\\/\\//],\n });\n if (!keyValidation.valid) {\n return {\n success: false,\n error: `Invalid storage key: ${keyValidation.errors.join(', ')}`,\n };\n }\n\n // Upload to storage\n const uploadResult = await storage.upload(key, file.data, {\n contentType: file.contentType,\n metadata: {\n productId,\n uploadedBy: options?.context ?? 'admin',\n altText: options?.altText ?? '',\n },\n });\n\n // TODO: Create database record\n // const mediaRecord = await createProductMediaRecord({\n // productId,\n // key,\n // url: uploadResult.url,\n // filename: file.filename,\n // contentType: file.contentType,\n // size: uploadResult.size,\n // altText: options?.altText,\n // description: options?.description,\n // tags: options?.tags,\n // sortOrder: index,\n // });\n\n // Generate signed URL for immediate use (or use uploadResult.url if available)\n const signedUrl =\n uploadResult.url ||\n (await storage.getUrl(key, { expiresIn: STORAGE_CONSTANTS.PRODUCT_URL_EXPIRY_SECONDS }));\n\n results.push({\n key,\n url: signedUrl,\n mediaId: `temp-${timestamp}-${index}`, // TODO: Replace with actual mediaRecord.id\n });\n }\n\n return {\n success: true,\n data: results,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to upload product media',\n };\n }\n}\n\n/**\n * Retrieve a product's media entries and attach signed URLs for client access.\n *\n * The returned media items include metadata (id, key, altText, sortOrder, contentType, size)\n * and a signed `url` valid for the configured expiry. If `options.variant` is provided and is\n * not `'public'`, the storage key is adjusted to include the variant before generating the URL.\n *\n * @param productId - The identifier of the product whose media should be fetched\n * @param options.context - Request context; affects visibility (e.g., `'admin'` may include deleted items)\n * @param options.variant - Optional variant key (`'thumbnail' | 'gallery' | 'hero' | 'public'`); when set and not `'public'` the storage key is suffixed with the variant for URL generation\n * @param options.expiresIn - Signed URL lifetime in seconds (defaults to 3600)\n * @returns On success, a `MediaActionResponse` containing an array of media items each with `id`, `key`, `url`, optional `altText`, `sortOrder`, `contentType`, and `size`; on failure, an error message describing the problem\n */\nexport async function getProductMediaAction(\n productId: string,\n options?: {\n context: 'admin' | 'customer' | 'vendor';\n variant?: 'thumbnail' | 'gallery' | 'hero' | 'public';\n expiresIn?: number;\n },\n): Promise<\n MediaActionResponse<\n Array<{\n id: string;\n key: string;\n url: string;\n altText?: string;\n sortOrder: number;\n contentType: string;\n size: number;\n }>\n >\n> {\n 'use server';\n\n try {\n // TODO: Get media from database\n // const productMedia = await getProductMediaFromDatabase(productId, {\n // includeDeleted: options?.context === 'admin',\n // });\n\n // Mock data for now\n const productMedia = [\n {\n id: 'media-1',\n key: `products/${productId}/images/hero.jpg`,\n altText: 'Product hero image',\n sortOrder: 0,\n contentType: 'image/jpeg',\n size: 1024000,\n },\n {\n id: 'media-2',\n key: `products/${productId}/images/gallery-1.jpg`,\n altText: 'Product gallery image 1',\n sortOrder: 1,\n contentType: 'image/jpeg',\n size: 856000,\n },\n ];\n\n const storage = getStorage();\n const expiresIn = options?.expiresIn ?? 3600; // 1 hour default\n\n // Generate signed URLs for all media\n const mediaWithUrls = await Promise.all(\n productMedia.map(async media => {\n let { key } = media;\n\n // For Cloudflare Images, append variant\n if (options?.variant && options.variant !== 'public') {\n key = `${media.key}/${options.variant}`;\n }\n\n const signedUrl = await storage.getUrl(key, { expiresIn });\n\n return {\n ...media,\n url: signedUrl,\n };\n }),\n );\n\n return {\n success: true,\n data: mediaWithUrls,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to get product media',\n };\n }\n}\n\n/**\n * Delete a product's media entry and remove its stored object when requested.\n *\n * Performs authentication and permission checks when enforcement is enabled. By default performs a soft delete of the media record; when `options.hardDelete` is true, also deletes the object from storage.\n *\n * @param options - Additional options controlling deletion behavior\n * @param options.context - Invocation context, either `'admin'` or `'vendor'`\n * @param options.hardDelete - If `true`, delete the storage object and perform a hard delete; if omitted or `false`, perform a soft delete\n * @returns A MediaActionResponse<void> indicating success. On failure `success` is `false` and `error` contains a descriptive message.\n */\nexport async function deleteProductMediaAction(\n productId: string,\n mediaId: string,\n options?: {\n context: 'admin' | 'vendor';\n hardDelete?: boolean;\n },\n): Promise<MediaActionResponse<void>> {\n 'use server';\n\n try {\n // Authentication check (configurable)\n const featureEnv = safeEnv();\n const session = await getSession();\n if (featureEnv.STORAGE_ENFORCE_AUTH && !session?.user) {\n return {\n success: false,\n error: 'Unauthorized: Authentication required',\n };\n }\n\n // Permission check (only when auth enforced)\n const hasPermission = session?.user\n ? await canUserManageProduct(session.user.id, productId)\n : false;\n if (featureEnv.STORAGE_ENFORCE_AUTH && !hasPermission) {\n return {\n success: false,\n error: 'Forbidden: Insufficient permissions',\n };\n }\n\n // TODO: Get media record from database\n // const mediaRecord = await getProductMediaById(mediaId);\n // if (!mediaRecord || mediaRecord.productId !== productId) {\n // throw new Error('Media not found');\n // }\n\n // Mock media record\n const mediaRecord = {\n id: mediaId,\n key: `products/${productId}/images/example.jpg`,\n productId,\n };\n\n if (options?.hardDelete) {\n // Delete from storage\n const storage = getStorage();\n await storage.delete(mediaRecord.key);\n\n // TODO: Hard delete from database\n // await deleteProductMediaRecord(mediaId);\n } else {\n // TODO: Soft delete in database\n // await softDeleteProductMediaRecord(mediaId, session.user.id);\n }\n\n return { success: true };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to delete product media',\n };\n }\n}\n\n/**\n * Reorders media items for a product.\n *\n * When storage authentication is enforced, this action requires an authenticated user\n * who has permission to manage the specified product; otherwise it returns an authorization error.\n *\n * @param productId - The ID of the product whose media order will be updated\n * @param mediaOrder - Array of objects mapping `mediaId` to the desired `sortOrder`\n * @param options.context - Optional caller context, e.g. 'admin' or 'vendor'\n * @returns An object with `success: true` when the reorder operation completes; otherwise `success: false` and an `error` message describing the failure\n */\nexport async function reorderProductMediaAction(\n productId: string,\n _mediaOrder: Array<{ mediaId: string; sortOrder: number }>,\n _options?: {\n context: 'admin' | 'vendor';\n },\n): Promise<MediaActionResponse<void>> {\n 'use server';\n\n try {\n // Authentication check (configurable)\n const featureEnv = safeEnv();\n const session = await getSession();\n if (featureEnv.STORAGE_ENFORCE_AUTH && !session?.user) {\n return {\n success: false,\n error: 'Unauthorized: Authentication required',\n };\n }\n\n // Permission check (only when auth enforced)\n const hasPermission = session?.user\n ? await canUserManageProduct(session.user.id, productId)\n : false;\n if (featureEnv.STORAGE_ENFORCE_AUTH && !hasPermission) {\n return {\n success: false,\n error: 'Forbidden: Insufficient permissions',\n };\n }\n\n // TODO: Update sort order in database\n // await updateMediaSortOrder(productId, mediaOrder);\n\n return { success: true };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to reorder product media',\n };\n }\n}\n\n/**\n * Generate presigned upload URLs for client-side direct uploads scoped to a product.\n *\n * @param productId - Product identifier used to scope generated storage keys\n * @param filenames - Desired filenames to include in generated storage keys\n * @param options - Optional settings for URL generation\n * @param options.context - The request context, e.g. 'admin' or 'vendor'\n * @param options.expiresIn - Signed URL lifetime in seconds; defaults to STORAGE_CONSTANTS.UPLOAD_URL_EXPIRY_SECONDS\n * @param options.maxSizeBytes - Optional maximum allowed upload size in bytes (informational; enforcement depends on storage provider)\n * @returns A MediaActionResponse containing an array of upload descriptors; each descriptor includes `filename`, `uploadUrl`, `key`, and optional form `fields`\n */\nexport async function getProductUploadPresignedUrlsAction(\n productId: string,\n filenames: string[],\n options?: {\n context: 'admin' | 'vendor';\n expiresIn?: number;\n maxSizeBytes?: number;\n },\n): Promise<\n MediaActionResponse<\n Array<{\n filename: string;\n uploadUrl: string;\n key: string;\n fields?: Record<string, string>;\n }>\n >\n> {\n 'use server';\n\n try {\n // Authentication check (configurable)\n const featureEnv = safeEnv();\n const session = await getSession();\n if (featureEnv.STORAGE_ENFORCE_AUTH && !session?.user) {\n return {\n success: false,\n error: 'Unauthorized: Authentication required',\n };\n }\n\n // Permission check (only when auth enforced)\n const hasPermission = session?.user\n ? await canUserManageProduct(session.user.id, productId)\n : false;\n if (featureEnv.STORAGE_ENFORCE_AUTH && !hasPermission) {\n return {\n success: false,\n error: 'Forbidden: Insufficient permissions',\n };\n }\n\n const storage = getStorage();\n const expiresIn = options?.expiresIn ?? STORAGE_CONSTANTS.UPLOAD_URL_EXPIRY_SECONDS;\n\n const uploadUrls = await Promise.all(\n filenames.map(async (filename, index) => {\n // Sanitize filename to prevent path traversal\n const sanitizedFilename = sanitizeStorageKey(filename);\n const timestamp = Date.now();\n const key = `products/${productId}/images/${timestamp}-${index}-${sanitizedFilename}`;\n\n // Validate storage key\n const keyValidation = validateStorageKey(key, {\n maxLength: 1024,\n forbiddenPatterns: [/\\.\\./, /\\/\\//],\n });\n if (!keyValidation.valid) {\n throw new Error(`Invalid storage key: ${keyValidation.errors.join(', ')}`);\n }\n\n // TODO: Use actual presigned POST URL when storage provider supports it\n // For now, use signed GET URL as placeholder\n const uploadUrl = await storage.getUrl(key, { expiresIn });\n\n return {\n filename,\n uploadUrl,\n key,\n // fields: presignedPost.fields, // For S3-style presigned POST\n };\n }),\n );\n\n return {\n success: true,\n data: uploadUrls,\n };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to generate upload URLs',\n };\n }\n}\n\n/**\n * Performs bulk metadata updates for a product's media items.\n *\n * @param productId - ID of the product whose media items will be updated\n * @param updates - List of update objects, each with a `mediaId` and optional `altText`, `description`, and `tags`\n * @param options - Optional operation context; expected values are `'admin'` or `'vendor'`\n * @returns A response object indicating success; on failure `error` contains a descriptive message (for example, `Unauthorized: Authentication required` or `Forbidden: Insufficient permissions`)\n */\nexport async function bulkUpdateProductMediaAction(\n productId: string,\n _updates: Array<{\n mediaId: string;\n altText?: string;\n description?: string;\n tags?: string[];\n }>,\n _options?: {\n context: 'admin' | 'vendor';\n },\n): Promise<MediaActionResponse<void>> {\n 'use server';\n\n try {\n // Authentication check (configurable)\n const featureEnv = safeEnv();\n const session = await getSession();\n if (featureEnv.STORAGE_ENFORCE_AUTH && !session?.user) {\n return {\n success: false,\n error: 'Unauthorized: Authentication required',\n };\n }\n\n // Permission check (only when auth enforced)\n const hasPermission = session?.user\n ? await canUserManageProduct(session.user.id, productId)\n : false;\n if (featureEnv.STORAGE_ENFORCE_AUTH && !hasPermission) {\n return {\n success: false,\n error: 'Forbidden: Insufficient permissions',\n };\n }\n\n // TODO: Bulk update in database\n // await bulkUpdateProductMedia(productId, updates);\n\n return { success: true };\n } catch (error) {\n return {\n success: false,\n error: error instanceof Error ? error.message : 'Failed to bulk update product media',\n };\n }\n}\n","/**\n * @fileoverview Blob upload server action\n *\n * Handles Vercel Blob uploads with authentication, validation, and callbacks.\n * Provides secure upload handling for Next.js server actions.\n *\n * @module @od-oneapp/storage/actions/blob-upload\n */\n\n'use server';\n\nimport { handleUpload } from '@vercel/blob/client';\nimport { logError } from '@repo/shared/logs';\n\n\nimport { safeEnv } from '../../env';\nimport { type HandleUploadConfig, type OnBeforeGenerateTokenResult } from '../../types';\nimport { validateCSRFToken } from '../auth-helpers';\n\nimport type { PutBlobResult } from '@vercel/blob';\n\n/**\n * Callback invoked before processing an upload or token generation\n * Allows validation and configuration of upload parameters\n */\nexport type OnBeforeGenerateToken = (\n pathname: string,\n clientPayload?: string,\n) => Promise<{\n allowed: boolean;\n token?: string;\n allowedContentTypes?: string[];\n maximumSizeInBytes?: number;\n tokenPayload?: string | null;\n}>;\n\n/**\n * Callback invoked after upload completes\n * Allows processing of uploaded file metadata\n */\nexport type OnUploadCompleted = (\n blob: {\n url: string;\n pathname: string;\n contentType?: string;\n contentDisposition?: string;\n size: number;\n },\n clientPayload?: string,\n) => Promise<void>;\n\n/**\n * Process a multipart/form-data upload via Vercel Blob, enforcing CSRF and environment token checks and invoking optional pre-token and post-upload hooks.\n *\n * @param request - Incoming HTTP Request expected to contain multipart/form-data for the upload.\n * @param config - Optional handlers:\n * - onBeforeGenerateToken(pathname, clientPayload): validate/configure a scoped client token and return `{ allowed, token?, allowedContentTypes?, maximumSizeInBytes?, tokenPayload? }`.\n * - onUploadCompleted(blob, clientPayload): receive completed upload metadata `{ url, pathname, contentType?, contentDisposition?, size }` and the original clientPayload.\n * @returns A Response with a JSON body. On success the body is the value returned by Vercel's `handleUpload`; on error the body is `{ error: string }` and the response status will be 403 for invalid CSRF or 500 for configuration/processing errors.\n */\nexport async function handleBlobUpload(\n request: Request,\n config?: HandleUploadConfig,\n): Promise<Response> {\n try {\n const blobEnv = safeEnv();\n\n // CSRF protection (only when enforced)\n if (blobEnv.STORAGE_ENFORCE_CSRF) {\n const csrfToken = request.headers.get('x-csrf-token');\n if (!csrfToken || !validateCSRFToken(csrfToken, request)) {\n return new Response(JSON.stringify({ error: 'Invalid CSRF token' }), {\n status: 403,\n headers: { 'Content-Type': 'application/json' },\n });\n }\n }\n const token = blobEnv.VERCEL_BLOB_READ_WRITE_TOKEN;\n\n if (!token) {\n return new Response(\n JSON.stringify({ error: 'VERCEL_BLOB_READ_WRITE_TOKEN not configured' }),\n {\n status: 500,\n headers: { 'Content-Type': 'application/json' },\n },\n );\n }\n\n // Parse the request body for handleUpload (Vercel blob client sends JSON events)\n const body = await request.json();\n\n // Use Vercel's handleUpload to manage the full upload flow\n const result = await handleUpload({\n body,\n token,\n request,\n onBeforeGenerateToken: async (\n pathname: string,\n clientPayload: string | null,\n _multipart: boolean,\n ) => {\n if (config?.onBeforeGenerateToken) {\n try {\n const result: OnBeforeGenerateTokenResult = await config.onBeforeGenerateToken(\n pathname,\n clientPayload ?? undefined,\n );\n if (!result.allowed) {\n throw new Error('Upload not allowed');\n }\n return {\n allowedContentTypes: result.allowedContentTypes,\n maximumSizeInBytes: result.maximumSizeInBytes,\n tokenPayload: result.tokenPayload,\n };\n } catch (error) {\n throw new Error(\n `Upload validation failed: ${error instanceof Error ? error.message : 'Unknown error'}`,\n );\n }\n }\n return {};\n },\n\n onUploadCompleted: async (payload: { blob: PutBlobResult; tokenPayload?: string | null }) => {\n if (config?.onUploadCompleted) {\n try {\n const { blob, tokenPayload } = payload;\n await config.onUploadCompleted(\n {\n url: blob.url,\n pathname: blob.pathname,\n contentType: blob.contentType,\n contentDisposition: blob.contentDisposition,\n size: (blob as PutBlobResult & { size?: number }).size ?? 0,\n },\n tokenPayload ?? undefined,\n );\n } catch (error) {\n // Don't fail the upload if callback fails\n // Use structured logging for better observability\n logError('onUploadCompleted callback failed', {\n error: error instanceof Error ? error.message : String(error),\n blob: {\n url: payload.blob.url,\n pathname: payload.blob.pathname,\n size: (payload.blob as PutBlobResult & { size?: number }).size,\n contentType: payload.blob.contentType,\n },\n clientPayload: payload.tokenPayload,\n timestamp: new Date().toISOString(),\n });\n }\n }\n },\n });\n\n return new Response(JSON.stringify(result), {\n status: 200,\n headers: { 'Content-Type': 'application/json' },\n });\n } catch (error) {\n return new Response(\n JSON.stringify({\n error: error instanceof Error ? error.message : 'Upload failed',\n }),\n { status: 500, headers: { 'Content-Type': 'application/json' } },\n );\n }\n}\n"],"mappings":"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"}
@@ -0,0 +1,70 @@
1
+ import { env, safeEnv } from "./index.mjs";
2
+ import { validateStorageKey } from "./keys.mjs";
3
+ import { ConfigError, DownloadError, NetworkError, ProviderError, QuotaInfo, StorageError, StorageErrorCode, UploadError, ValidationError, ValidationOptions, createStorageError, formatFileSize, getErrorCode, getQuotaInfo, isQuotaExceeded, isRetryableError, parseFileSize, validateFileSize, validateMimeType, validateUploadOptions } from "./validation.mjs";
4
+ import { BlobListResponse, BulkDeleteResponse, BulkMoveResponse, ClientUploadOptions, CloudflareImagesBatchToken, CloudflareImagesListOptions, CloudflareImagesSigningKey, CloudflareImagesStats, CloudflareImagesTransformOptions, CloudflareImagesVariant, DirectUploadResponse, EnhancedR2Credentials, HandleUploadConfig, ListOptions, MediaActionResponse, MultiStorageConfig, MultipartUploadOptions, OnBeforeGenerateTokenResult, PresignedUploadUrl, R2Credentials, StorageCapabilities, StorageConfig, StorageObject, StorageProvider, StorageProviderType, StreamUploadOptions, UploadOptions, UploadProgress, VercelBlobOptions } from "./types.mjs";
5
+ import "./client.mjs";
6
+ import { t as VercelBlobProvider } from "./vercel-blob-07Sx0Akn.mjs";
7
+ import { _ as getOptimalPartSize, a as describeProviderCapabilities, b as MultiStorageManager, c as getProviderCapabilities, d as hasCapability, f as validateProviderCapabilities, g as createMultipartUploadManager, h as MultipartUploadState, i as checkProviderSuitability, l as hasAllCapabilities, m as MultipartUploadResult, n as checkProviderHealth, o as getBestProvider, p as MultipartUploadManager, r as storageHealthCheck, s as getCapabilityMatrix, t as HealthCheckResult, u as hasAnyCapability, v as hasMultipartSupport, x as CloudflareImagesProvider, y as CloudflareR2Provider } from "./health-check-im_huJ59.mjs";
8
+
9
+ //#region src/server.d.ts
10
+ declare function validateStorageConfig(config: StorageConfig): {
11
+ valid: boolean;
12
+ errors: string[];
13
+ warnings: string[];
14
+ };
15
+ declare function getProviderCapabilitiesFromConfig(config: StorageConfig): StorageCapabilities;
16
+ declare function createStorageProvider(config: StorageConfig): StorageProvider;
17
+ declare function resetStorageState(): void;
18
+ declare function getStorage(): StorageProvider;
19
+ declare function initializeStorage(): StorageProvider;
20
+ declare function initializeMultiStorage(): MultiStorageManager;
21
+ declare function getMultiStorage(): MultiStorageManager;
22
+ declare const storage: {
23
+ delete: (key: string) => Promise<void>;
24
+ download: (key: string) => Promise<Blob>;
25
+ exists: (key: string) => Promise<boolean>;
26
+ getMetadata: (key: string) => Promise<StorageObject>;
27
+ getUrl: (key: string, options?: {
28
+ expiresIn?: number;
29
+ }) => Promise<string>;
30
+ list: (options?: ListOptions) => Promise<StorageObject[]>;
31
+ upload: (key: string, data: ArrayBuffer | Blob | Buffer | File | ReadableStream, options?: UploadOptions) => Promise<StorageObject>;
32
+ createMultipartUpload: (key: string, options?: UploadOptions) => Promise<{
33
+ uploadId: string;
34
+ key: string;
35
+ }>;
36
+ uploadPart: (uploadId: string, partNumber: number, data: ArrayBuffer | Blob | Buffer, options?: UploadOptions) => Promise<{
37
+ etag: string;
38
+ partNumber: number;
39
+ }>;
40
+ completeMultipartUpload: (uploadId: string, parts: Array<{
41
+ etag: string;
42
+ partNumber: number;
43
+ }>) => Promise<StorageObject>;
44
+ abortMultipartUpload: (uploadId: string) => Promise<void>;
45
+ getPresignedUploadUrl: (key: string, options?: {
46
+ expiresIn?: number;
47
+ contentType?: string;
48
+ }) => Promise<PresignedUploadUrl>;
49
+ getCapabilities: () => StorageCapabilities;
50
+ };
51
+ declare const multiStorage: {
52
+ delete: (key: string) => Promise<void>;
53
+ download: (key: string) => Promise<Blob>;
54
+ exists: (key: string) => Promise<boolean>;
55
+ getMetadata: (key: string) => Promise<StorageObject>;
56
+ getProvider: (name: string) => StorageProvider | undefined;
57
+ getProviderNames: () => string[];
58
+ getUrl: (key: string, options?: {
59
+ expiresIn?: number;
60
+ }) => Promise<string>;
61
+ list: (options?: ListOptions & {
62
+ provider?: string;
63
+ }) => Promise<StorageObject[]>;
64
+ upload: (key: string, data: ArrayBuffer | Blob | Buffer | File | ReadableStream, options?: UploadOptions & {
65
+ provider?: string;
66
+ }) => Promise<StorageObject>;
67
+ };
68
+ //#endregion
69
+ export { BlobListResponse, BulkDeleteResponse, BulkMoveResponse, ClientUploadOptions, CloudflareImagesBatchToken, CloudflareImagesListOptions, CloudflareImagesProvider, CloudflareImagesSigningKey, CloudflareImagesStats, CloudflareImagesTransformOptions, CloudflareImagesVariant, CloudflareR2Provider, ConfigError, DirectUploadResponse, DownloadError, EnhancedR2Credentials, HandleUploadConfig, type HealthCheckResult, ListOptions, MediaActionResponse, MultiStorageConfig, MultiStorageManager, MultipartUploadManager, MultipartUploadOptions, type MultipartUploadResult, type MultipartUploadState, NetworkError, OnBeforeGenerateTokenResult, PresignedUploadUrl, ProviderError, type QuotaInfo, R2Credentials, StorageCapabilities, StorageConfig, StorageError, StorageErrorCode, StorageObject, StorageProvider, StorageProviderType, StreamUploadOptions, UploadError, UploadOptions, UploadProgress, ValidationError, type ValidationOptions, VercelBlobOptions, VercelBlobProvider, checkProviderHealth, checkProviderSuitability, createMultipartUploadManager, createStorageError, createStorageProvider, describeProviderCapabilities, env, formatFileSize, getBestProvider, getCapabilityMatrix, getErrorCode, getMultiStorage, getOptimalPartSize, getProviderCapabilities, getProviderCapabilitiesFromConfig, getQuotaInfo, getStorage, hasAllCapabilities, hasAnyCapability, hasCapability, hasMultipartSupport, initializeMultiStorage, initializeStorage, isQuotaExceeded, isRetryableError, multiStorage, parseFileSize, resetStorageState, safeEnv, storage, storageHealthCheck, validateFileSize, validateMimeType, validateProviderCapabilities, validateStorageConfig, validateStorageKey, validateUploadOptions };
70
+ //# sourceMappingURL=server.d.mts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server.d.mts","names":[],"sources":["../src/server.ts"],"mappings":";;;;;;;;;iBAyGgB,qBAAA,CAAsB,MAAA,EAAQ,aAAA;EAC5C,KAAA;EACA,MAAA;EACA,QAAA;AAAA;AAAA,iBAmFc,iCAAA,CAAkC,MAAA,EAAQ,aAAA,GAAgB,mBAAA;AAAA,iBAsE1D,qBAAA,CAAsB,MAAA,EAAQ,aAAA,GAAgB,eAAA;AAAA,iBAiD9C,iBAAA,CAAA;AAAA,iBA+DA,UAAA,CAAA,GAAc,eAAA;AAAA,iBAgBd,iBAAA,CAAA,GAAqB,eAAA;AAAA,iBAgFrB,sBAAA,CAAA,GAA0B,mBAAA;AAAA,iBAiG1B,eAAA,CAAA,GAAmB,mBAAA;AAAA,cAWtB,OAAA;2BACS,OAAA;6BACE,OAAA,CAAA,IAAA;2BACF,OAAA;gCACK,OAAA,CAAA,aAAA;wBACL,OAAA;IAAc,SAAA;EAAA,MAAoB,OAAA;mBACrC,WAAA,KAAW,OAAA,CAAA,aAAA;wBAEf,IAAA,EACL,WAAA,GAAc,IAAA,GAAO,MAAA,GAAS,IAAA,GAAO,cAAA,EAAc,OAAA,GAC/C,aAAA,KAAa,OAAA,CAAA,aAAA;uCAIU,OAAA,GAAY,aAAA,KAAa,OAAA;;;;iCAS1C,UAAA,UACE,IAAA,EACZ,WAAA,GAAc,IAAA,GAAO,MAAA,EAAM,OAAA,GACvB,aAAA,KAAa,OAAA;;;;8CAUP,KAAA,EACT,KAAA;IAAQ,IAAA;IAAc,UAAA;EAAA,OAAqB,OAAA,CAAA,aAAA;8CASb,OAAA;uCASJ,OAAA;IAAc,SAAA;IAAoB,WAAA;EAAA,MAAsB,OAAA,CATpD,kBAAA;;;cA4B5B,YAAA;2BACS,OAAA;6BACE,OAAA,CAAA,IAAA;2BACF,OAAA;gCACK,OAAA,CAAA,aAAA;iCACC,eAAA;;wBAEN,OAAA;IAAc,SAAA;EAAA,MAAoB,OAAA;mBACrC,WAAA;IAAgB,QAAA;EAAA,MAAmB,OAAA,CAAA,aAAA;wBAEvC,IAAA,EACL,WAAA,GAAc,IAAA,GAAO,MAAA,GAAS,IAAA,GAAO,cAAA,EAAc,OAAA,GAC/C,aAAA;IAAkB,QAAA;EAAA,MAAmB,OAAA,CAAA,aAAA;AAAA"}