@nextlyhq/storage-uploadthing 0.0.2-alpha.60 → 0.0.2-alpha.64

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.
@@ -1 +1 @@
1
- {"version":3,"sources":["../../nextly/dist/chunk-2ROPMX7N.mjs","../../nextly/dist/chunk-7P6ASYW6.mjs","../../nextly/dist/chunk-VM3CQBI2.mjs","../../nextly/dist/storage/index.mjs","../src/adapter.ts","../src/plugin.ts"],"names":["UTApi"],"mappings":"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","file":"index.cjs","sourcesContent":["// src/storage/adapters/local-adapter.ts\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\n\n// src/storage/adapters/base-adapter.ts\nvar BaseStorageAdapter = class {\n /**\n * Get adapter info including capabilities.\n *\n * Default implementation that auto-detects capabilities by checking\n * if getSignedUrl and getPresignedUploadUrl methods are implemented.\n * Override in subclasses for more accurate capability reporting.\n *\n * @returns Adapter info with type, name, and capability flags\n */\n getInfo() {\n const hasSignedUrls = \"getSignedUrl\" in this && typeof this.getSignedUrl === \"function\";\n const hasClientUploads = \"getPresignedUploadUrl\" in this && typeof this.getPresignedUploadUrl === \"function\";\n return {\n type: this.getType(),\n name: this.constructor.name,\n supportsSignedUrls: hasSignedUrls,\n supportsClientUploads: hasClientUploads\n };\n }\n /**\n * Sanitize filename to prevent directory traversal and storage issues.\n *\n * Security measures:\n * - Remove path separators (/, \\)\n * - Keep only basename (no directories)\n * - Replace problematic characters with hyphens\n * - Preserve alphanumeric, dots, underscores, hyphens\n *\n * @param filename - Original filename to sanitize\n * @returns Sanitized filename safe for storage\n *\n * @example\n * ```typescript\n * this.sanitizeFilename('../../../etc/passwd') // 'passwd'\n * this.sanitizeFilename('my file (1).jpg') // 'my-file--1-.jpg'\n * this.sanitizeFilename('photo.jpg') // 'photo.jpg'\n * ```\n */\n sanitizeFilename(filename) {\n const basename = filename.split(/[/\\\\]/).pop() || filename;\n return basename.replace(/[^a-zA-Z0-9._-]/g, \"-\");\n }\n /**\n * Generate a unique storage key with date-based prefix.\n *\n * Creates keys in format: {folder}/{year}/{month}/{uuid}-{sanitized-filename}\n * This provides:\n * - Unique keys via UUID to prevent collisions\n * - Date-based organization for easier management\n * - Readable filenames for debugging\n *\n * @param filename - Original filename (will be sanitized)\n * @param folder - Optional folder/prefix for organizing uploads\n * @returns Generated storage key\n *\n * @example\n * ```typescript\n * this.generateKey('photo.jpg')\n * // '2026/01/abc-123-...-photo.jpg'\n *\n * this.generateKey('doc.pdf', 'documents')\n * // 'documents/2026/01/abc-123-...-doc.pdf'\n * ```\n */\n generateKey(filename, folder) {\n const sanitized = this.sanitizeFilename(filename);\n const uuid = crypto.randomUUID();\n const date = /* @__PURE__ */ new Date();\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const prefix = folder ? `${folder}/${year}/${month}` : `${year}/${month}`;\n return `${prefix}/${uuid}-${sanitized}`;\n }\n};\n\n// src/storage/adapters/local-adapter.ts\nvar gitignoreUpdated = false;\nvar LocalStorageAdapter = class extends BaseStorageAdapter {\n basePath;\n baseUrl;\n constructor(config) {\n super();\n this.basePath = path.resolve(config.basePath);\n this.baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n }\n /**\n * Upload file to local disk.\n * Creates directories as needed and writes the file buffer.\n */\n async upload(buffer, options) {\n const key = this.generateKey(options.filename, options.folder);\n const fullPath = this.resolveAndValidate(key);\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\n await fs.writeFile(fullPath, buffer);\n await this.ensureGitignore();\n return {\n url: this.getPublicUrl(key),\n path: key\n };\n }\n /**\n * Delete file from local disk.\n * Silently succeeds if the file doesn't exist.\n */\n async delete(filePath) {\n let fullPath;\n try {\n fullPath = this.resolveAndValidate(filePath);\n } catch {\n return;\n }\n try {\n await fs.unlink(fullPath);\n } catch (err) {\n if (err.code !== \"ENOENT\") {\n throw err;\n }\n }\n }\n /**\n * Bulk delete files from local disk.\n * Uses parallel unlinks with Promise.allSettled for best performance.\n */\n async bulkDelete(filePaths) {\n const results = await Promise.allSettled(\n filePaths.map(async (filePath) => {\n await this.delete(filePath);\n return filePath;\n })\n );\n const successful = [];\n const failed = [];\n results.forEach((result, index) => {\n if (result.status === \"fulfilled\") {\n successful.push(filePaths[index]);\n } else {\n failed.push({\n filePath: filePaths[index],\n error: result.reason?.message || \"Unknown error\"\n });\n }\n });\n return { successful, failed };\n }\n /**\n * Check if file exists on local disk.\n */\n async exists(filePath) {\n try {\n const fullPath = this.resolveAndValidate(filePath);\n await fs.access(fullPath);\n return true;\n } catch {\n return false;\n }\n }\n /**\n * Get public URL for a file.\n * Returns baseUrl + relative path for Next.js static file serving.\n */\n getPublicUrl(filePath) {\n const cleanPath = filePath.replace(/^\\/+/, \"\");\n return `${this.baseUrl}/${cleanPath}`;\n }\n /**\n * Get storage type identifier.\n */\n getType() {\n return \"local\";\n }\n /**\n * Read file contents from local disk.\n * Returns the file buffer, or null if file not found.\n */\n async read(filePath) {\n try {\n const fullPath = this.resolveAndValidate(filePath);\n return await fs.readFile(fullPath);\n } catch {\n return null;\n }\n }\n // ============================================================\n // Private Helpers\n // ============================================================\n /**\n * Resolve a relative file path to an absolute path within basePath.\n * Throws if the resolved path would escape basePath (path traversal attack).\n */\n resolveAndValidate(filePath) {\n const sanitized = filePath.replace(/^[/\\\\]+/, \"\").replace(/\\.\\.[/\\\\]/g, \"\");\n const fullPath = path.resolve(this.basePath, sanitized);\n if (!fullPath.startsWith(this.basePath)) {\n throw new Error(\n `Path traversal detected: ${filePath} resolves outside of storage directory`\n );\n }\n return fullPath;\n }\n /**\n * Auto-add the uploads directory to .gitignore on first upload.\n * Prevents accidentally committing uploaded files to git.\n */\n async ensureGitignore() {\n if (gitignoreUpdated) return;\n gitignoreUpdated = true;\n try {\n const projectRoot = path.resolve(this.basePath, \"..\", \"..\");\n const gitignorePath = path.join(projectRoot, \".gitignore\");\n let content = \"\";\n try {\n content = await fs.readFile(gitignorePath, \"utf-8\");\n } catch {\n }\n const uploadsDirRelative = path.relative(projectRoot, this.basePath);\n const ignorePattern = uploadsDirRelative + \"/\";\n if (!content.includes(ignorePattern)) {\n const newEntry = `\n# Nextly local uploads (auto-added)\n${ignorePattern}\n`;\n await fs.writeFile(gitignorePath, content + newEntry, \"utf-8\");\n }\n } catch {\n }\n }\n};\n\n// src/storage/adapters/local-plugin.ts\nfunction localStorage(config) {\n if (config.enabled === false) {\n return {\n name: \"local-storage\",\n type: \"local\",\n collections: {},\n adapter: null\n };\n }\n const adapter = new LocalStorageAdapter({\n basePath: config.basePath ?? \"./public/uploads\",\n baseUrl: config.baseUrl ?? \"/uploads\"\n });\n return {\n name: \"local-storage\",\n type: \"local\",\n collections: config.collections,\n adapter\n // Local storage doesn't support presigned URLs or signed downloads\n };\n}\n\nexport {\n BaseStorageAdapter,\n LocalStorageAdapter,\n localStorage\n};\n","var __defProp = Object.defineProperty;\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\n\nexport {\n __export\n};\n","import {\n LocalStorageAdapter\n} from \"./chunk-2ROPMX7N.mjs\";\n\n// src/storage/storage.ts\nvar MediaStorage = class {\n /** Registered storage plugins by name */\n plugins = /* @__PURE__ */ new Map();\n /** Storage adapter per collection */\n collectionAdapters = /* @__PURE__ */ new Map();\n /** Storage configuration per collection */\n collectionConfigs = /* @__PURE__ */ new Map();\n /** Local storage adapter (always available as fallback) */\n localAdapter;\n /**\n * Create a new MediaStorage instance.\n *\n * @param config - Optional configuration for storage initialization\n */\n constructor(config) {\n this.localAdapter = new LocalStorageAdapter({\n basePath: config?.local?.uploadDir ?? \"./public/uploads\",\n baseUrl: config?.local?.publicPath ?? \"/uploads\"\n });\n if (config?.plugins) {\n for (const plugin of config.plugins) {\n this.registerPlugin(plugin);\n }\n }\n }\n // ============================================================\n // Plugin Registration\n // ============================================================\n /**\n * Register a storage plugin.\n *\n * Plugins provide storage adapters for specific collections.\n * When a collection is registered with a plugin, uploads for that\n * collection will be routed to the plugin's adapter.\n *\n * @param plugin - The storage plugin to register\n *\n * @example\n * ```typescript\n * const storage = new MediaStorage();\n *\n * storage.registerPlugin(s3Storage({\n * bucket: 'my-bucket',\n * region: 'us-east-1',\n * collections: {\n * media: true,\n * 'private-docs': { prefix: 'private/' }\n * }\n * }));\n * ```\n */\n registerPlugin(plugin) {\n if (!plugin.adapter) {\n return;\n }\n this.plugins.set(plugin.name, plugin);\n for (const [collectionSlug, config] of Object.entries(plugin.collections)) {\n const collectionConfig = typeof config === \"boolean\" ? {} : config;\n this.collectionAdapters.set(collectionSlug, plugin.adapter);\n this.collectionConfigs.set(collectionSlug, collectionConfig);\n }\n }\n // ============================================================\n // Adapter Resolution\n // ============================================================\n /**\n * Check if any storage adapter is configured.\n *\n * @returns True if at least one storage plugin is registered\n */\n hasAdapter() {\n return true;\n }\n /**\n * Get the storage adapter if available, or null if not configured.\n *\n * Unlike getAdapter(), this method does not throw an error if no storage\n * is configured. Useful for optional storage scenarios.\n *\n * @param collection - The collection slug (optional)\n * @returns The storage adapter instance, or null if not configured\n */\n getAdapterOrNull(collection) {\n if (collection && this.collectionAdapters.has(collection)) {\n return this.collectionAdapters.get(collection);\n }\n return this.localAdapter;\n }\n /**\n * Get the storage adapter for a specific collection.\n *\n * If a plugin is configured for the collection, returns the plugin's adapter.\n * Otherwise, returns the default adapter (first registered plugin).\n *\n * @param collection - The collection slug (optional)\n * @returns The appropriate storage adapter\n * @throws Error if no storage plugin is configured\n */\n getAdapterForCollection(collection) {\n if (collection && this.collectionAdapters.has(collection)) {\n return this.collectionAdapters.get(collection);\n }\n return this.localAdapter;\n }\n /**\n * Get configuration for a specific collection.\n *\n * @param collection - The collection slug\n * @returns The collection's storage configuration, or undefined\n */\n getCollectionConfig(collection) {\n return this.collectionConfigs.get(collection);\n }\n // ============================================================\n // Core Storage Operations\n // ============================================================\n /**\n * Upload file to appropriate storage based on collection.\n *\n * Routes the upload to the correct adapter based on collection\n * configuration. Applies collection-specific prefix if configured.\n *\n * @param buffer - The file buffer to upload\n * @param options - Upload options including filename, mimeType, collection\n * @returns Upload result with URL and path\n *\n * @example\n * ```typescript\n * const result = await storage.upload(buffer, {\n * filename: 'photo.jpg',\n * mimeType: 'image/jpeg',\n * collection: 'media'\n * });\n * console.log(result.url); // Public URL\n * console.log(result.path); // Storage path for deletion\n * ```\n */\n async upload(buffer, options) {\n const adapter = this.getAdapterForCollection(options.collection);\n const config = options.collection ? this.getCollectionConfig(options.collection) : void 0;\n const uploadOptions = { ...options };\n if (config?.prefix) {\n uploadOptions.folder = config.prefix + (options.folder || \"\");\n }\n return adapter.upload(buffer, uploadOptions);\n }\n /**\n * Delete file from storage.\n *\n * Determines correct adapter based on collection.\n *\n * @param filePath - The storage path/key of the file\n * @param collection - The collection slug (optional, for routing)\n */\n async delete(filePath, collection) {\n const adapter = this.getAdapterForCollection(collection);\n return adapter.delete(filePath);\n }\n /**\n * Bulk delete files from storage.\n * Uses adapter's native bulkDelete if available, otherwise falls back to\n * sequential individual deletes in chunks of 10.\n */\n async bulkDelete(filePaths, collection) {\n const adapter = this.getAdapterForCollection(collection);\n if (adapter.bulkDelete) {\n return adapter.bulkDelete(filePaths);\n }\n const successful = [];\n const failed = [];\n const chunkSize = 10;\n for (let i = 0; i < filePaths.length; i += chunkSize) {\n const chunk = filePaths.slice(i, i + chunkSize);\n const results = await Promise.allSettled(\n chunk.map((fp) => adapter.delete(fp))\n );\n results.forEach((result, idx) => {\n const fp = chunk[idx];\n if (result.status === \"fulfilled\") {\n successful.push(fp);\n } else {\n failed.push({\n filePath: fp,\n error: result.reason instanceof Error ? result.reason.message : String(result.reason)\n });\n }\n });\n }\n return { successful, failed };\n }\n /**\n * Check if file exists in storage.\n *\n * @param filePath - The storage path/key to check\n * @param collection - The collection slug (optional, for routing)\n * @returns True if file exists\n */\n async exists(filePath, collection) {\n const adapter = this.getAdapterForCollection(collection);\n return adapter.exists(filePath);\n }\n /**\n * Get public URL for file.\n *\n * @param filePath - The storage path/key\n * @param collection - The collection slug (optional, for routing)\n * @returns Public URL to access the file\n */\n getPublicUrl(filePath, collection) {\n const adapter = this.getAdapterForCollection(collection);\n return adapter.getPublicUrl(filePath);\n }\n /**\n * Get storage type for a collection.\n *\n * @param collection - The collection slug (optional)\n * @returns Storage type identifier ('s3', 'vercel-blob')\n */\n getStorageType(collection) {\n const adapter = this.getAdapterForCollection(collection);\n return adapter.getType();\n }\n // ============================================================\n // Client Upload Support\n // ============================================================\n /**\n * Check if collection supports client-side uploads.\n *\n * Client-side uploads allow direct-to-storage uploads, bypassing\n * the server. This is essential for serverless platforms with\n * request body size limits (e.g., Vercel's 4.5MB limit).\n *\n * @param collection - The collection slug\n * @returns True if client uploads are enabled and supported\n */\n supportsClientUploads(collection) {\n const config = this.getCollectionConfig(collection);\n if (!config?.clientUploads) return false;\n const adapter = this.getAdapterForCollection(collection);\n const info = adapter.getInfo?.();\n return info?.supportsClientUploads ?? false;\n }\n /**\n * Get client upload URL for direct-to-storage uploads.\n *\n * Generates a pre-signed URL that allows the client to upload\n * directly to the storage backend, bypassing the server.\n *\n * Only available if:\n * 1. Collection is configured with `clientUploads: true`\n * 2. The storage adapter supports client uploads\n *\n * @param filename - Original filename\n * @param mimeType - File MIME type\n * @param collection - Collection slug\n * @returns Client upload data, or null if not supported\n *\n * @example\n * ```typescript\n * // Server-side: generate upload URL\n * const uploadData = await storage.getClientUploadUrl(\n * 'photo.jpg',\n * 'image/jpeg',\n * 'media'\n * );\n *\n * // Client-side: upload directly to storage\n * await fetch(uploadData.uploadUrl, {\n * method: uploadData.method,\n * headers: uploadData.headers,\n * body: file\n * });\n * ```\n */\n async getClientUploadUrl(filename, mimeType, collection) {\n if (!this.supportsClientUploads(collection)) {\n return null;\n }\n for (const plugin of this.plugins.values()) {\n if (collection in plugin.collections && plugin.getClientUploadUrl) {\n return plugin.getClientUploadUrl(filename, mimeType, collection);\n }\n }\n return null;\n }\n // ============================================================\n // Signed Download Support\n // ============================================================\n /**\n * Check if collection supports signed download URLs.\n *\n * @param collection - The collection slug\n * @returns True if signed downloads are enabled and supported\n */\n supportsSignedDownloads(collection) {\n const config = this.getCollectionConfig(collection);\n if (!config?.signedDownloads) return false;\n const adapter = this.getAdapterForCollection(collection);\n const info = adapter.getInfo?.();\n return info?.supportsSignedUrls ?? false;\n }\n /**\n * Get signed download URL for secure file access.\n *\n * Generates a time-limited signed URL for accessing files in\n * private storage buckets. Only works if:\n * 1. Collection is configured with `signedDownloads: true`\n * 2. The storage adapter supports signed URLs\n *\n * @param filePath - Storage path/key of the file\n * @param collection - Collection slug\n * @param expiresIn - URL expiry time in seconds (optional)\n * @returns Signed URL, or null if not supported\n *\n * @example\n * ```typescript\n * const signedUrl = await storage.getSignedDownloadUrl(\n * 'private/doc.pdf',\n * 'private-docs',\n * 3600 // 1 hour\n * );\n * ```\n */\n async getSignedDownloadUrl(filePath, collection, expiresIn) {\n if (!this.supportsSignedDownloads(collection)) {\n return null;\n }\n const config = this.getCollectionConfig(collection);\n for (const plugin of this.plugins.values()) {\n if (collection in plugin.collections && plugin.getSignedDownloadUrl) {\n return plugin.getSignedDownloadUrl(\n filePath,\n expiresIn ?? config?.signedUrlExpiresIn ?? 3600\n );\n }\n }\n return null;\n }\n // ============================================================\n // Accessor Methods\n // ============================================================\n /**\n * Get the default storage adapter.\n *\n * @returns The default storage adapter (first registered plugin)\n * @throws Error if no storage plugin is configured\n */\n getDefaultAdapter() {\n return this.localAdapter;\n }\n /**\n * Get list of registered plugins.\n *\n * @returns Array of registered storage plugins\n */\n getPlugins() {\n return Array.from(this.plugins.values());\n }\n /**\n * Get the underlying storage adapter for a collection.\n *\n * Useful for passing to registerServices() which requires IStorageAdapter.\n *\n * @param collection - The collection slug (optional)\n * @returns The storage adapter instance\n */\n getAdapter(collection) {\n return this.getAdapterForCollection(collection);\n }\n /**\n * Check if a collection has a configured storage adapter.\n *\n * @param collection - The collection slug\n * @returns True if a plugin is configured for this collection\n */\n hasCollectionAdapter(collection) {\n return this.collectionAdapters.has(collection);\n }\n /**\n * Get list of collections with configured storage.\n *\n * @returns Array of collection slugs that have plugin storage\n */\n getConfiguredCollections() {\n return Array.from(this.collectionAdapters.keys());\n }\n /**\n * Check if any storage plugin is configured.\n *\n * @returns True if at least one storage plugin is registered\n */\n hasPlugins() {\n return this.plugins.size > 0;\n }\n};\nvar storageInstance = null;\nfunction initializeMediaStorage(config) {\n storageInstance = new MediaStorage(config);\n return storageInstance;\n}\nfunction getMediaStorage() {\n if (!storageInstance) {\n storageInstance = new MediaStorage();\n }\n return storageInstance;\n}\nfunction resetMediaStorage() {\n storageInstance = null;\n}\n\n// src/storage/image-processor.ts\nvar sharpModule = null;\nasync function getSharp() {\n if (!sharpModule) {\n sharpModule = (await import(\"sharp\")).default;\n }\n return sharpModule;\n}\nvar ImageProcessor = class {\n /**\n * Get image metadata without loading full image\n */\n async getMetadata(buffer) {\n const sharp = await getSharp();\n const metadata = await sharp(buffer).metadata();\n return {\n width: metadata.width || 0,\n height: metadata.height || 0,\n format: metadata.format || \"unknown\",\n size: buffer.length\n };\n }\n /**\n * Generate thumbnail (300x300 by default, cropped to center)\n *\n * Uses \"cover\" fit to fill the entire 300x300 area while maintaining aspect ratio\n */\n async generateThumbnail(buffer, size = 300) {\n const sharp = await getSharp();\n const processed = await sharp(buffer).resize(size, size, {\n fit: \"cover\",\n // Crop to fill entire area\n position: \"center\"\n // Crop from center\n }).jpeg({ quality: 80, progressive: true }).toBuffer({ resolveWithObject: true });\n return {\n buffer: processed.data,\n metadata: {\n width: processed.info.width,\n height: processed.info.height,\n format: processed.info.format,\n size: processed.data.length\n }\n };\n }\n /**\n * Optimize image (compress, convert to WebP if beneficial)\n *\n * Strategy:\n * - Small images (<100KB) and already WebP: return as-is\n * - Otherwise: convert to WebP with quality 80\n */\n async optimize(buffer, quality = 80) {\n const sharp = await getSharp();\n const metadata = await sharp(buffer).metadata();\n if (buffer.length < 100 * 1024 && metadata.format === \"webp\") {\n return {\n buffer,\n metadata: {\n width: metadata.width || 0,\n height: metadata.height || 0,\n format: metadata.format,\n size: buffer.length\n }\n };\n }\n const processed = await sharp(buffer).webp({ quality, effort: 4 }).toBuffer({ resolveWithObject: true });\n return {\n buffer: processed.data,\n metadata: {\n width: processed.info.width,\n height: processed.info.height,\n format: \"webp\",\n size: processed.data.length\n }\n };\n }\n /**\n * Resize image to specific dimensions\n *\n * @param maxWidth Maximum width (maintains aspect ratio)\n * @param maxHeight Maximum height (maintains aspect ratio)\n */\n async resize(buffer, maxWidth, maxHeight) {\n const sharp = await getSharp();\n const processed = await sharp(buffer).resize(maxWidth, maxHeight, {\n fit: \"inside\",\n // Fit within bounds, maintaining aspect ratio\n withoutEnlargement: true\n // Don't upscale small images\n }).toBuffer({ resolveWithObject: true });\n return {\n buffer: processed.data,\n metadata: {\n width: processed.info.width,\n height: processed.info.height,\n format: processed.info.format,\n size: processed.data.length\n }\n };\n }\n /**\n * Resize an image with focal point awareness and format conversion.\n *\n * When fit is 'cover' and a focal point is set, the crop anchors at that\n * point instead of center. Supports format conversion ('auto' outputs webp\n * for jpeg/png/tiff sources, keeps original for gif).\n */\n async resizeWithFocalPoint(buffer, options) {\n const sharp = await getSharp();\n const quality = options.quality ?? 80;\n const metadata = await sharp(buffer).metadata();\n const originalFormat = metadata.format || \"jpeg\";\n let outputFormat = originalFormat;\n if (options.format && options.format !== \"auto\") {\n outputFormat = options.format;\n } else if (options.format === \"auto\") {\n const convertibleFormats = [\"jpeg\", \"png\", \"tiff\", \"jpg\"];\n if (convertibleFormats.includes(originalFormat)) {\n outputFormat = \"webp\";\n }\n }\n let pipeline = sharp(buffer);\n const srcW = metadata.width;\n const srcH = metadata.height;\n const tgtW = options.width;\n const tgtH = options.height;\n const hasFocalPoint = options.fit === \"cover\" && (options.focalX !== void 0 || options.focalY !== void 0) && tgtW !== void 0 && tgtH !== void 0 && srcW !== void 0 && srcH !== void 0 && tgtW > 0 && tgtH > 0 && srcW > 0 && srcH > 0;\n if (hasFocalPoint) {\n const fx = (options.focalX ?? 50) / 100;\n const fy = (options.focalY ?? 50) / 100;\n const tgtAspect = tgtW / tgtH;\n let cropW;\n let cropH;\n if (srcW / srcH > tgtAspect) {\n cropH = srcH;\n cropW = Math.round(srcH * tgtAspect);\n } else {\n cropW = srcW;\n cropH = Math.round(srcW / tgtAspect);\n }\n let left = Math.round(fx * srcW - cropW / 2);\n let top = Math.round(fy * srcH - cropH / 2);\n left = Math.max(0, Math.min(srcW - cropW, left));\n top = Math.max(0, Math.min(srcH - cropH, top));\n pipeline = pipeline.extract({ left, top, width: cropW, height: cropH }).resize(tgtW, tgtH, { fit: \"fill\" });\n } else {\n pipeline = pipeline.resize(\n options.width || void 0,\n options.height || void 0,\n {\n fit: options.fit,\n position: \"center\",\n withoutEnlargement: true\n }\n );\n }\n switch (outputFormat) {\n case \"webp\":\n pipeline = pipeline.webp({ quality, effort: 4 });\n break;\n case \"jpeg\":\n case \"jpg\":\n pipeline = pipeline.jpeg({ quality, progressive: true });\n outputFormat = \"jpeg\";\n break;\n case \"png\":\n pipeline = pipeline.png({ quality });\n break;\n case \"avif\":\n pipeline = pipeline.avif({ quality });\n break;\n default:\n break;\n }\n const result = await pipeline.toBuffer({ resolveWithObject: true });\n return {\n buffer: result.data,\n width: result.info.width,\n height: result.info.height,\n format: outputFormat,\n size: result.data.length\n };\n }\n /**\n * Check if buffer is a valid image\n */\n async isValidImage(buffer) {\n try {\n const sharp = await getSharp();\n await sharp(buffer).metadata();\n return true;\n } catch {\n return false;\n }\n }\n /**\n * Get image dimensions quickly (without full processing)\n */\n async getDimensions(buffer) {\n try {\n const sharp = await getSharp();\n const metadata = await sharp(buffer).metadata();\n if (metadata.width && metadata.height) {\n return { width: metadata.width, height: metadata.height };\n }\n return null;\n } catch {\n return null;\n }\n }\n};\nvar processorInstance = null;\nfunction getImageProcessor() {\n if (!processorInstance) {\n processorInstance = new ImageProcessor();\n }\n return processorInstance;\n}\nfunction resetImageProcessor() {\n processorInstance = null;\n}\n\n// src/storage/image-sizes.ts\nfunction getExtensionForFormat(format) {\n switch (format) {\n case \"jpeg\":\n case \"jpg\":\n return \"jpg\";\n case \"webp\":\n return \"webp\";\n case \"png\":\n return \"png\";\n case \"avif\":\n return \"avif\";\n default:\n return format;\n }\n}\nfunction getMimeTypeForFormat(format) {\n switch (format) {\n case \"jpeg\":\n case \"jpg\":\n return \"image/jpeg\";\n case \"webp\":\n return \"image/webp\";\n case \"png\":\n return \"image/png\";\n case \"avif\":\n return \"image/avif\";\n default:\n return `image/${format}`;\n }\n}\nfunction buildVariantFilename(originalFilename, sizeName, format) {\n const lastDot = originalFilename.lastIndexOf(\".\");\n const baseName = lastDot > 0 ? originalFilename.substring(0, lastDot) : originalFilename;\n const ext = getExtensionForFormat(format);\n return `${baseName}-${sizeName}.${ext}`;\n}\nasync function generateImageSizes(originalBuffer, originalFilename, sizes, uploadFn, options = {}) {\n if (sizes.length === 0) return {};\n const processor = getImageProcessor();\n const results = {};\n for (const sizeConfig of sizes) {\n try {\n if (!sizeConfig.width && !sizeConfig.height) continue;\n const resized = await processor.resizeWithFocalPoint(originalBuffer, {\n width: sizeConfig.width ?? void 0,\n height: sizeConfig.height ?? void 0,\n fit: sizeConfig.fit,\n quality: sizeConfig.quality,\n format: sizeConfig.format,\n focalX: options.focalX ?? void 0,\n focalY: options.focalY ?? void 0\n });\n const variantFilename = buildVariantFilename(\n originalFilename,\n sizeConfig.name,\n resized.format\n );\n const mimeType = getMimeTypeForFormat(resized.format);\n const uploadResult = await uploadFn(resized.buffer, {\n filename: variantFilename,\n mimeType,\n folder: options.folder,\n collection: options.collection\n });\n results[sizeConfig.name] = {\n url: uploadResult.url,\n path: uploadResult.path,\n width: resized.width,\n height: resized.height,\n filesize: resized.size,\n mimeType,\n filename: variantFilename\n };\n } catch (error) {\n console.warn(\n `[ImageSizes] Failed to generate size \"${sizeConfig.name}\":`,\n error instanceof Error ? error.message : error\n );\n }\n }\n return results;\n}\nasync function deleteImageSizes(sizes, deleteFn) {\n if (!sizes) return;\n const paths = Object.values(sizes).map((v) => v.path).filter(Boolean);\n await Promise.allSettled(paths.map((path) => deleteFn(path)));\n}\n\n// src/storage/retry.ts\nvar DEFAULT_OPTIONS = {\n maxAttempts: 3,\n baseDelayMs: 1e3,\n maxDelayMs: 3e4,\n backoffFactor: 2,\n jitter: true\n};\nfunction isTransientError(error) {\n if (!error) return false;\n if (error instanceof Error) {\n const message = error.message.toLowerCase();\n const name = error.name.toLowerCase();\n if (message.includes(\"timeout\") || message.includes(\"timed out\") || message.includes(\"etimedout\") || message.includes(\"econnreset\") || message.includes(\"econnrefused\") || message.includes(\"enotfound\") || message.includes(\"enetunreach\") || message.includes(\"socket hang up\") || message.includes(\"network\") || name.includes(\"timeout\") || name.includes(\"abort\")) {\n return true;\n }\n if (message.includes(\"rate limit\") || message.includes(\"too many\")) {\n return true;\n }\n }\n const errorAny = error;\n const metadata = errorAny.$metadata;\n if (errorAny.statusCode || errorAny.status || metadata?.httpStatusCode) {\n const status = errorAny.statusCode || errorAny.status || metadata?.httpStatusCode;\n const statusNum = typeof status === \"number\" ? status : Number(status);\n if (statusNum === 429 || statusNum >= 500 && statusNum < 600) {\n return true;\n }\n }\n if (errorAny.code) {\n const code = String(errorAny.code).toLowerCase();\n if (code.includes(\"timeout\") || code.includes(\"throttl\") || code.includes(\"serviceunavailable\") || code.includes(\"slowdown\") || code === \"econnreset\" || code === \"epipe\") {\n return true;\n }\n }\n return false;\n}\nfunction calculateDelay(attempt, options) {\n const { baseDelayMs, maxDelayMs, backoffFactor, jitter } = options;\n const exponentialDelay = baseDelayMs * Math.pow(backoffFactor, attempt - 1);\n const cappedDelay = Math.min(exponentialDelay, maxDelayMs);\n if (jitter) {\n const jitterAmount = cappedDelay * Math.random() * 0.5;\n return Math.floor(cappedDelay + jitterAmount);\n }\n return Math.floor(cappedDelay);\n}\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\nasync function withRetry(fn, options = {}) {\n const config = { ...DEFAULT_OPTIONS, ...options };\n const { maxAttempts, shouldRetry, onRetry } = {\n ...config,\n shouldRetry: options.shouldRetry ?? isTransientError,\n onRetry: options.onRetry\n };\n let lastError;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n const isLastAttempt = attempt >= maxAttempts;\n const canRetry = !isLastAttempt && shouldRetry(error, attempt);\n if (!canRetry) {\n throw error;\n }\n const delayMs = calculateDelay(attempt, config);\n if (onRetry) {\n onRetry(error, attempt, delayMs);\n }\n await sleep(delayMs);\n }\n }\n throw lastError;\n}\nfunction createRetryable(fn, options = {}) {\n return (...args) => withRetry(() => fn(...args), options);\n}\n\n// src/storage/svg-security.ts\nvar SVG_CSP_HEADER = \"script-src 'none'; style-src 'unsafe-inline'\";\nfunction isSvgMimeType(mimeType) {\n return mimeType.toLowerCase().trim() === \"image/svg+xml\";\n}\nfunction getSvgSecurityHeaders() {\n return {\n \"Content-Security-Policy\": SVG_CSP_HEADER,\n \"X-Content-Type-Options\": \"nosniff\"\n };\n}\n\n// src/storage/env-config.ts\nasync function ensureEnvLoaded() {\n if (typeof process !== \"undefined\" && !process.env._NEXTLY_ENV_LOADED) {\n try {\n const dotenv = await import(\"dotenv\");\n dotenv.config();\n process.env._NEXTLY_ENV_LOADED = \"true\";\n } catch {\n }\n }\n}\nasync function getStorageFromEnv() {\n await ensureEnvLoaded();\n const blobToken = process.env.BLOB_READ_WRITE_TOKEN;\n if (blobToken) {\n try {\n const pkg = \"@nextlyhq/storage-vercel-blob\";\n const { vercelBlobStorage } = await import(\n /* webpackIgnore: true */\n pkg\n );\n console.log(\n \"[Nextly] Storage: Vercel Blob (auto-detected from BLOB_READ_WRITE_TOKEN)\"\n );\n return [\n vercelBlobStorage({ token: blobToken, collections: { media: true } })\n ];\n } catch {\n console.warn(\n \"[Nextly] BLOB_READ_WRITE_TOKEN set but @nextlyhq/storage-vercel-blob not installed. Run: pnpm add @nextlyhq/storage-vercel-blob\"\n );\n }\n }\n const s3Bucket = process.env.S3_BUCKET;\n if (s3Bucket) {\n const region = process.env.S3_REGION;\n const accessKeyId = process.env.AWS_ACCESS_KEY_ID;\n const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;\n if (!region || !accessKeyId || !secretAccessKey) {\n const missing = [];\n if (!region) missing.push(\"S3_REGION\");\n if (!accessKeyId) missing.push(\"AWS_ACCESS_KEY_ID\");\n if (!secretAccessKey) missing.push(\"AWS_SECRET_ACCESS_KEY\");\n console.warn(\n `[Nextly] S3_BUCKET set but missing: ${missing.join(\", \")}. Falling back to local storage.`\n );\n } else {\n try {\n const pkg = \"@nextlyhq/storage-s3\";\n const { s3Storage } = await import(\n /* webpackIgnore: true */\n pkg\n );\n console.log(\"[Nextly] Storage: S3 (auto-detected from S3_BUCKET)\");\n return [\n s3Storage({\n bucket: s3Bucket,\n region,\n accessKeyId,\n secretAccessKey,\n endpoint: process.env.S3_ENDPOINT,\n // eslint-disable-next-line turbo/no-undeclared-env-vars\n publicUrl: process.env.S3_PUBLIC_URL,\n // eslint-disable-next-line turbo/no-undeclared-env-vars\n forcePathStyle: process.env.S3_FORCE_PATH_STYLE === \"true\",\n collections: { media: true }\n })\n ];\n } catch {\n console.warn(\n \"[Nextly] S3_BUCKET set but @nextlyhq/storage-s3 not installed. Run: pnpm add @nextlyhq/storage-s3\"\n );\n }\n }\n }\n const uploadthingToken = process.env.UPLOADTHING_TOKEN;\n if (uploadthingToken) {\n try {\n const pkg = \"@nextlyhq/storage-uploadthing\";\n const { uploadthingStorage } = await import(\n /* webpackIgnore: true */\n pkg\n );\n console.log(\n \"[Nextly] Storage: Uploadthing (auto-detected from UPLOADTHING_TOKEN)\"\n );\n return [\n uploadthingStorage({\n token: uploadthingToken,\n collections: { media: true }\n })\n ];\n } catch {\n console.warn(\n \"[Nextly] UPLOADTHING_TOKEN set but @nextlyhq/storage-uploadthing not installed. Run: pnpm add @nextlyhq/storage-uploadthing\"\n );\n }\n }\n const { localStorage: localStorage2 } = await import(\"./local-plugin-5QEQ632L.mjs\");\n console.log(\n \"[Nextly] Storage: Local disk (no cloud env vars detected, using ./public/uploads)\"\n );\n return [localStorage2({ collections: { media: true } })];\n}\n\nexport {\n ImageProcessor,\n getImageProcessor,\n resetImageProcessor,\n MediaStorage,\n initializeMediaStorage,\n getMediaStorage,\n resetMediaStorage,\n generateImageSizes,\n deleteImageSizes,\n isTransientError,\n withRetry,\n createRetryable,\n SVG_CSP_HEADER,\n isSvgMimeType,\n getSvgSecurityHeaders,\n getStorageFromEnv\n};\n","import {\n ImageProcessor,\n MediaStorage,\n SVG_CSP_HEADER,\n createRetryable,\n deleteImageSizes,\n generateImageSizes,\n getImageProcessor,\n getMediaStorage,\n getStorageFromEnv,\n getSvgSecurityHeaders,\n initializeMediaStorage,\n isSvgMimeType,\n isTransientError,\n resetImageProcessor,\n resetMediaStorage,\n withRetry\n} from \"../chunk-VM3CQBI2.mjs\";\nimport {\n BaseStorageAdapter,\n LocalStorageAdapter,\n localStorage\n} from \"../chunk-2ROPMX7N.mjs\";\nimport \"../chunk-7P6ASYW6.mjs\";\nexport {\n BaseStorageAdapter,\n ImageProcessor,\n LocalStorageAdapter,\n MediaStorage,\n SVG_CSP_HEADER,\n createRetryable,\n deleteImageSizes,\n generateImageSizes,\n getImageProcessor,\n getMediaStorage,\n getStorageFromEnv,\n getSvgSecurityHeaders,\n initializeMediaStorage,\n isSvgMimeType,\n isTransientError,\n localStorage,\n resetImageProcessor,\n resetMediaStorage,\n withRetry\n};\n","/**\n * Uploadthing Storage Adapter\n *\n * Implements the Nextly storage adapter interface using Uploadthing's UTApi\n * for server-side file operations. Files are served via Uploadthing's CDN.\n *\n * @example\n * ```typescript\n * const adapter = new UploadthingStorageAdapter({ token: process.env.UPLOADTHING_TOKEN });\n * const result = await adapter.upload(buffer, {\n * filename: 'photo.jpg',\n * mimeType: 'image/jpeg',\n * });\n * // result.url = 'https://utfs.io/f/abc123-photo.jpg'\n * ```\n */\n\nimport { BaseStorageAdapter } from \"nextly/storage\";\nimport type {\n UploadOptions,\n UploadResult,\n BulkDeleteResult,\n} from \"nextly/storage\";\nimport { UTApi } from \"uploadthing/server\";\n\n// ============================================================\n// Adapter Implementation\n// ============================================================\n\nexport class UploadthingStorageAdapter extends BaseStorageAdapter {\n private readonly utapi: UTApi;\n\n constructor(config: { token?: string }) {\n super();\n // UTApi reads UPLOADTHING_TOKEN from env if not provided\n this.utapi = new UTApi({\n ...(config.token ? { token: config.token } : {}),\n });\n }\n\n /**\n * Upload file to Uploadthing.\n * Creates a File object from the buffer and uploads via UTApi.\n */\n async upload(buffer: Buffer, options: UploadOptions): Promise<UploadResult> {\n const sanitized = this.sanitizeFilename(options.filename);\n\n // UTApi.uploadFiles expects File objects\n // Cast buffer to BlobPart to satisfy TS 5.9 strict ArrayBuffer typing\n const file = new File([buffer as unknown as BlobPart], sanitized, {\n type: options.mimeType,\n });\n\n // uploadFiles returns UploadFileResult[] — one result per file.\n // Default contentDisposition flipped from \"inline\" to \"attachment\".\n // An \"inline\" disposition lets the browser render the file\n // in-context (HTML, SVG, PDF with embedded JS) which can land as\n // XSS or drive-by; \"attachment\" forces the download dialog so the\n // user has to opt in to opening it. Adopters who genuinely want\n // inline rendering can still pass `contentDisposition: \"inline\"`\n // explicitly.\n const results = await this.utapi.uploadFiles([file], {\n contentDisposition: options.contentDisposition ?? \"attachment\",\n });\n\n // results is an array of { data: { key, url, ... } | null, error: ... | null }\n const result = results[0] as {\n data: { key: string; url: string } | null;\n error: { message: string } | null;\n };\n\n if (!result?.data) {\n const errorMsg = result?.error?.message ?? \"Unknown error\";\n throw new Error(`Uploadthing upload failed: ${errorMsg}`);\n }\n\n return {\n url: result.data.url,\n // Use the file key as the storage path (needed for deletion)\n path: result.data.key,\n };\n }\n\n /**\n * Delete file from Uploadthing by its file key.\n */\n async delete(filePath: string): Promise<void> {\n try {\n await this.utapi.deleteFiles([filePath], { keyType: \"fileKey\" });\n } catch {\n // Silently ignore deletion errors (file may already be gone)\n }\n }\n\n /**\n * Bulk delete files from Uploadthing.\n * UTApi natively supports batch deletion.\n */\n async bulkDelete(filePaths: string[]): Promise<BulkDeleteResult> {\n try {\n await this.utapi.deleteFiles(filePaths, { keyType: \"fileKey\" });\n return {\n successful: filePaths,\n failed: [],\n };\n } catch (error: unknown) {\n // If bulk delete fails entirely, report all as failed\n const message =\n error instanceof Error ? error.message : \"Bulk delete failed\";\n return {\n successful: [],\n failed: filePaths.map(fp => ({\n filePath: fp,\n error: message,\n })),\n };\n }\n }\n\n /**\n * Check if file exists on Uploadthing.\n * Uses getFileUrls - if it returns data with URLs, the file exists.\n */\n async exists(filePath: string): Promise<boolean> {\n try {\n const result = await this.utapi.getFileUrls([filePath], {\n keyType: \"fileKey\",\n });\n // getFileUrls returns { data: readonly [{ url, key }] }\n const items = Array.from(result.data);\n return items.length > 0 && !!items[0]?.url;\n } catch {\n return false;\n }\n }\n\n /**\n * Get public URL for a file.\n * Uploadthing files are served from utfs.io CDN.\n * The URL is stored at upload time, so this reconstructs it from the key.\n */\n getPublicUrl(filePath: string): string {\n // Uploadthing URLs follow the pattern: https://utfs.io/f/{fileKey}\n return `https://utfs.io/f/${filePath}`;\n }\n\n /**\n * Get storage type identifier.\n */\n getType(): string {\n return \"uploadthing\";\n }\n\n /**\n * Keep filename sanitization local so this adapter remains stable\n * even if upstream base adapter type declarations drift.\n */\n protected sanitizeFilename(filename: string): string {\n const basename = filename.split(/[/\\\\]/).pop() || filename;\n return basename.replace(/[^a-zA-Z0-9._-]/g, \"-\");\n }\n}\n","/**\n * Uploadthing Storage Plugin\n *\n * Factory function that creates a storage plugin for Uploadthing.\n * Returns a StoragePlugin that can be registered with MediaStorage.\n *\n * @example\n * ```typescript\n * import { uploadthingStorage } from '@nextlyhq/storage-uploadthing'\n * import { defineConfig } from 'nextly/config'\n *\n * export default defineConfig({\n * storage: [\n * uploadthingStorage({\n * token: process.env.UPLOADTHING_TOKEN,\n * collections: { media: true }\n * })\n * ]\n * })\n * ```\n */\n\nimport type { StoragePlugin } from \"nextly/storage\";\n\nimport { UploadthingStorageAdapter } from \"./adapter\";\nimport type { UploadthingStorageConfig } from \"./types\";\n\n/**\n * Create an Uploadthing storage plugin for Nextly.\n *\n * @param config - Uploadthing storage configuration\n * @returns A StoragePlugin that MediaStorage can register\n */\nexport function uploadthingStorage(\n config: UploadthingStorageConfig\n): StoragePlugin {\n // Handle disabled plugin\n if (config.enabled === false) {\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: {},\n adapter: null as unknown as StoragePlugin[\"adapter\"],\n };\n }\n\n // Token from config or env\n const token = config.token ?? process.env.UPLOADTHING_TOKEN;\n\n if (!token) {\n console.warn(\n \"[Nextly] Uploadthing token not provided. Set UPLOADTHING_TOKEN env var or pass token in config.\"\n );\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: {},\n adapter: null as unknown as StoragePlugin[\"adapter\"],\n };\n }\n\n const adapter = new UploadthingStorageAdapter({ token });\n\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: config.collections,\n adapter,\n // Uploadthing supports client-side uploads via its own pattern\n // but we don't implement getClientUploadUrl here as it requires\n // Uploadthing's specific route handler setup\n };\n}\n"]}
1
+ {"version":3,"sources":["../../nextly/dist/chunk-HJ5IQPOY.mjs","../../nextly/dist/chunk-W77AQUUQ.mjs","../../nextly/dist/chunk-C2VHLQKK.mjs","../../nextly/dist/chunk-IJCL4XOT.mjs","../../nextly/dist/chunk-7P6ASYW6.mjs","../../nextly/dist/chunk-ANFXYTYN.mjs","../../nextly/dist/storage/index.mjs","../../nextly/dist/storage/fetch-stored-bytes.mjs","../src/adapter.ts","../src/plugin.ts"],"names":["createRequire","UTApi"],"mappings":"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","file":"index.cjs","sourcesContent":["// src/database/errors.ts\nvar DbError = class extends Error {\n kind;\n dialect;\n code;\n meta;\n cause;\n constructor(args) {\n super(args.message);\n this.name = \"DbError\";\n this.kind = args.kind;\n this.dialect = args.dialect;\n this.code = args.code;\n this.meta = args.meta;\n this.cause = args.cause;\n }\n};\nfunction isDbError(err) {\n if (!err || typeof err !== \"object\") return false;\n const obj = err;\n return obj.name === \"DbError\" && typeof obj.kind === \"string\";\n}\nfunction toDbError(dialect, error, meta) {\n if (isDbError(error)) return error;\n const code = safeCode(error);\n const rawMsg = error != null && typeof error === \"object\" && \"message\" in error ? error.message : void 0;\n const message = typeof rawMsg === \"string\" ? rawMsg : \"Database error\";\n let kind = \"internal\";\n switch (dialect) {\n case \"postgresql\":\n kind = mapPostgresCodeToKind(code);\n break;\n case \"mysql\":\n kind = mapMySqlCodeToKind(code);\n break;\n case \"sqlite\":\n kind = mapSqliteCodeToKind(code);\n break;\n }\n if ((dialect === \"mysql\" || dialect === \"sqlite\") && (kind === \"constraint\" || kind === \"internal\") && isLikelyUniqueViolationMessage(dialect, message)) {\n kind = \"unique-violation\";\n }\n if (kind === \"internal\" && code) {\n const c = code.toUpperCase();\n if (c === \"ECONNRESET\" || c.includes(\"PROTOCOL_CONNECTION_LOST\"))\n kind = \"connection-lost\";\n else if (c === \"ETIMEDOUT\") kind = \"timeout\";\n }\n return new DbError({ message, kind, dialect, code, meta, cause: error });\n}\nfunction safeCode(error) {\n if (!error || typeof error !== \"object\") return void 0;\n const e = error;\n let c = e.code ?? e.sqlState ?? e.state ?? e.errno ?? void 0;\n if (!c && typeof e.originalError?.code === \"string\") c = e.originalError.code;\n if (!c && typeof e.cause?.code === \"string\") c = e.cause.code;\n if (!c && typeof e.severity === \"string\" && typeof e.code === \"string\")\n c = e.code;\n if (typeof c === \"number\") return String(c);\n if (typeof c === \"string\" && c.length > 0) return c;\n return void 0;\n}\nfunction mapPostgresCodeToKind(code) {\n if (!code) return \"internal\";\n const c = code.toUpperCase();\n if (c === \"40P01\") return \"deadlock\";\n if (c === \"40001\") return \"serialization-failure\";\n if (c === \"57014\") return \"timeout\";\n if (c === \"23505\") return \"unique-violation\";\n if (c === \"23503\") return \"fk-violation\";\n if (c === \"23502\") return \"not-null-violation\";\n if (c === \"42601\") return \"syntax\";\n if (c.startsWith(\"08\") || c === \"57P01\") return \"connection-lost\";\n if (c.startsWith(\"23\")) return \"constraint\";\n return \"internal\";\n}\nfunction mapMySqlCodeToKind(code) {\n if (!code) return \"internal\";\n const c = code.toUpperCase();\n if (c === \"1213\" || c === \"ER_LOCK_DEADLOCK\") return \"deadlock\";\n if (c === \"1205\" || c === \"ER_LOCK_WAIT_TIMEOUT\") return \"timeout\";\n if (c === \"1062\" || c === \"ER_DUP_ENTRY\") return \"unique-violation\";\n if (c === \"23000\") return \"constraint\";\n if (c === \"1064\" || c === \"ER_PARSE_ERROR\") return \"syntax\";\n if (c === \"1452\" || c === \"ER_NO_REFERENCED_ROW_2\") return \"fk-violation\";\n if (c === \"1451\" || c === \"ER_ROW_IS_REFERENCED_2\") return \"fk-violation\";\n if (c === \"ECONNRESET\" || c.includes(\"PROTOCOL_CONNECTION_LOST\"))\n return \"connection-lost\";\n if (c === \"ETIMEDOUT\") return \"timeout\";\n return \"internal\";\n}\nfunction mapSqliteCodeToKind(code) {\n if (!code) return \"internal\";\n const c = code.toUpperCase();\n if (c === \"SQLITE_BUSY\" || c === \"SQLITE_LOCKED\") return \"deadlock\";\n if (c === \"SQLITE_CONSTRAINT_UNIQUE\") return \"unique-violation\";\n if (c === \"SQLITE_CONSTRAINT_PRIMARYKEY\") return \"unique-violation\";\n if (c === \"SQLITE_CONSTRAINT_FOREIGNKEY\") return \"fk-violation\";\n if (c === \"SQLITE_CONSTRAINT_NOTNULL\") return \"not-null-violation\";\n if (c === \"SQLITE_INTERRUPT\") return \"timeout\";\n if (c === \"SQLITE_ERROR\") return \"syntax\";\n if (c.startsWith(\"SQLITE_CONSTRAINT\")) return \"constraint\";\n return \"internal\";\n}\nfunction isLikelyUniqueViolationMessage(dialect, message) {\n const m = message.toLowerCase();\n if (dialect === \"mysql\") {\n return m.includes(\"duplicate entry\") || m.includes(\"duplicate key\");\n }\n if (dialect === \"sqlite\") {\n return m.includes(\"unique constraint failed\");\n }\n return false;\n}\n\n// src/errors/error-codes.ts\nvar NEXTLY_ERROR_STATUS = {\n VALIDATION_ERROR: 400,\n INVALID_INPUT: 400,\n AUTH_REQUIRED: 401,\n AUTH_INVALID_CREDENTIALS: 401,\n TOKEN_EXPIRED: 401,\n FORBIDDEN: 403,\n // The schema builder is off in this environment (production by default).\n // Separate from FORBIDDEN: the caller's permissions are not the problem.\n BUILDER_DISABLED: 403,\n NOT_FOUND: 404,\n CONFLICT: 409,\n DUPLICATE: 409,\n RATE_LIMITED: 429,\n PAYLOAD_TOO_LARGE: 413,\n UNSUPPORTED_MEDIA_TYPE: 415,\n // 422: understood, well-formed, and refused on a rule the caller can act on.\n // Deliberately NOT added to CANONICAL_CODE_FOR_STATUS -- that list drives the\n // status -> code direction, where 422 stays mapped to INVALID_INPUT.\n BUSINESS_RULE_VIOLATION: 422,\n INTERNAL_ERROR: 500,\n DATABASE_ERROR: 500,\n EXTERNAL_SERVICE_ERROR: 502,\n SERVICE_UNAVAILABLE: 503,\n // Outbound-fetch safety (utils/validate-external-url): a URL refused for SSRF\n // safety, and a fetch that timed out / exceeded the size cap / failed to decode.\n EXTERNAL_URL_BLOCKED: 400,\n EXTERNAL_REQUEST_FAILED: 502,\n FILENAME_INVALID: 400,\n EXTENSION_BLOCKED: 400,\n MIME_BLOCKED: 415,\n MIME_NOT_ALLOWED: 415,\n SIZE_EXCEEDED: 413,\n // A stored object exceeded the cap a READ was given, which is a different\n // question from SIZE_EXCEEDED above: that one refuses an upload the caller\n // is sending, this one refuses to buffer an object already stored. Kept\n // apart so a caller discriminating on the code cannot match both.\n STORAGE_READ_TOO_LARGE: 413,\n // A stored object did not answer within the deadline the read was given. 504\n // rather than 500 because the failure is the BACKEND not answering, and a\n // caller can act on that difference: a gateway timeout is worth retrying, an\n // internal error is not.\n STORAGE_READ_TIMEOUT: 504,\n // The store answered, and answered badly. 502 rather than 500 because the\n // fault is UPSTREAM of this process: a caller can retry it, and an operator\n // reading the log needs to look at the bucket rather than at this service.\n // Distinct from the timeout above, which never got an answer at all.\n STORAGE_READ_UNREACHABLE: 502,\n MAGIC_BYTE_MISMATCH: 400,\n SVG_SANITIZATION_FAILED: 400,\n UNSUPPORTED_FOR_BACKEND: 415,\n // Plan B — schema bookkeeping consolidation.\n NEXTLY_LEGACY_BOOKKEEPING_DETECTED: 409,\n NEXTLY_UPGRADE_TABLE_NAME_COLLISION: 409,\n NEXTLY_UPGRADE_IN_PROGRESS: 409,\n // Plan C2 — nextly migrate phases.\n NEXTLY_MIGRATE_LOCK_BUSY: 409,\n NEXTLY_BASELINE_LOCK_NOT_HELD: 409,\n NEXTLY_RESOLVE_LOCK_NOT_HELD: 409,\n // Boot refused to serve: the migrate lock stayed held past the wait deadline,\n // so this process never established whether the schema matches the code. 503\n // rather than 409 — a load balancer should take the instance out of rotation\n // and retry it, which is exactly the recovery this refusal wants.\n NEXTLY_BOOT_MIGRATIONS_NOT_RUN: 503,\n // Still running rather than refused. 503 for the same reason: retry shortly.\n NEXTLY_BOOT_MIGRATIONS_PENDING: 503,\n NEXTLY_CORE_DESTRUCTIVE_REFUSED: 409,\n NEXTLY_MIGRATION_DRIFT: 409,\n NEXTLY_MIGRATION_APPLY_FAILED: 500,\n // Plan C3 — migrate:resolve recovery command.\n NEXTLY_MIGRATION_FILE_MISSING: 404,\n NEXTLY_MIGRATION_SNAPSHOT_MISSING: 404,\n NEXTLY_MIGRATION_RESOLVE_DRIFT: 409,\n NEXTLY_MIGRATION_RESOLVE_PRECONDITION: 409,\n // Plan D — UI schema support.\n NEXTLY_UI_SCHEMA_INVALID: 400,\n NEXTLY_SCHEMA_SLUG_COLLISION: 409,\n NEXTLY_SCHEMA_RELATION_TARGET_MISSING: 400,\n // Plugin platform (P2b) — schema extend (contributes.extend) + relations (D15).\n NEXTLY_SCHEMA_EXTEND_TARGET_UNKNOWN: 400,\n NEXTLY_SCHEMA_EXTEND_FIELD_DUPLICATE: 409,\n NEXTLY_SCHEMA_CROSS_PLUGIN_RELATION: 409,\n // Plugin platform (P2c) — framework remap (.rename()).\n NEXTLY_SCHEMA_RENAME_UNKNOWN_TARGET: 400,\n // Plugin platform — a declared admin.clientConfig that cannot be delivered\n // to the browser, refused at boot rather than serialized mangled.\n NEXTLY_PLUGIN_CLIENT_CONFIG_INVALID: 500,\n // Plugin platform — a contributed admin widget that cannot be delivered to\n // the browser. Refused at boot because it is serialized into the ONE\n // `/api/admin-meta/workspace` payload: a value `JSON.stringify` throws on\n // fails that request for every admin, not just the widget's own card.\n NEXTLY_PLUGIN_ADMIN_WIDGET_INVALID: 500,\n // Plugin platform (P0) — boot-time plugin dependency/version resolution.\n PLUGIN_RESOLUTION_ERROR: 500,\n // Plugin platform (P4) — contributes.routes collection (D25).\n NEXTLY_ROUTE_COLLISION: 409,\n NEXTLY_ROUTE_INVALID_PATH: 400,\n // An email transport whose library is an optional peer dependency the host\n // has not installed. 503 rather than 500: the request is not malformed and\n // nothing is broken, the install simply cannot carry it out yet, and the\n // remedy is one command on the server rather than a change by the caller.\n NEXTLY_EMAIL_TRANSPORT_UNAVAILABLE: 503,\n // The tooling that compiles `nextly.config.ts` is an optional peer the host\n // has not installed. 503 rather than 500 for the same reason as the mail\n // transport above: nothing is broken and the request is not malformed, the\n // install simply cannot carry it out until one command is run.\n NEXTLY_CONFIG_TOOLING_UNAVAILABLE: 503\n};\nvar CANONICAL_CODE_FOR_STATUS = [\n \"VALIDATION_ERROR\",\n \"AUTH_REQUIRED\",\n \"FORBIDDEN\",\n \"NOT_FOUND\",\n \"CONFLICT\",\n \"PAYLOAD_TOO_LARGE\",\n \"UNSUPPORTED_MEDIA_TYPE\",\n \"RATE_LIMITED\",\n \"EXTERNAL_SERVICE_ERROR\",\n \"SERVICE_UNAVAILABLE\"\n];\nvar LEGACY_UNPROCESSABLE_STATUS = 422;\nvar STATUS_TO_CODE = {\n ...Object.fromEntries(\n CANONICAL_CODE_FOR_STATUS.map((code) => [NEXTLY_ERROR_STATUS[code], code])\n ),\n [LEGACY_UNPROCESSABLE_STATUS]: \"INVALID_INPUT\"\n};\nfunction statusToErrorCode(statusCode) {\n return STATUS_TO_CODE[statusCode] ?? \"INTERNAL_ERROR\";\n}\nvar GENERIC_PUBLIC_MESSAGE = {\n VALIDATION_ERROR: \"Validation failed.\",\n INVALID_INPUT: \"The request could not be processed.\",\n AUTH_REQUIRED: \"Authentication required.\",\n FORBIDDEN: \"You don't have permission to perform this action.\",\n NOT_FOUND: \"Not found.\",\n CONFLICT: \"The resource has changed since you last loaded it. Please refresh and try again.\",\n DUPLICATE: \"Resource already exists.\",\n PAYLOAD_TOO_LARGE: \"The request is too large.\",\n UNSUPPORTED_MEDIA_TYPE: \"That file type is not supported.\",\n RATE_LIMITED: \"Too many requests. Please try again later.\",\n EXTERNAL_SERVICE_ERROR: \"An upstream service failed.\",\n SERVICE_UNAVAILABLE: \"The service is temporarily unavailable.\",\n INTERNAL_ERROR: \"An unexpected error occurred.\"\n};\nfunction genericPublicMessage(code) {\n return GENERIC_PUBLIC_MESSAGE[code] ?? GENERIC_PUBLIC_MESSAGE.INTERNAL_ERROR;\n}\n\n// src/errors/nextly-error.ts\nvar DB_ERROR_MAPPING = {\n \"unique-violation\": {\n code: \"DUPLICATE\",\n statusCode: 409,\n publicMessage: \"Resource already exists.\"\n },\n \"fk-violation\": {\n code: \"VALIDATION_ERROR\",\n statusCode: 400,\n publicMessage: \"Referenced record does not exist.\"\n },\n \"not-null-violation\": {\n code: \"VALIDATION_ERROR\",\n statusCode: 400,\n publicMessage: \"A required field is missing.\"\n },\n constraint: {\n code: \"VALIDATION_ERROR\",\n statusCode: 400,\n publicMessage: \"The provided data violates a constraint.\"\n },\n deadlock: {\n code: \"CONFLICT\",\n statusCode: 409,\n publicMessage: \"The operation could not be completed. Please retry.\"\n },\n \"serialization-failure\": {\n code: \"CONFLICT\",\n statusCode: 409,\n publicMessage: \"The operation could not be completed. Please retry.\"\n },\n timeout: {\n code: \"DATABASE_ERROR\",\n statusCode: 500,\n publicMessage: \"The operation timed out. Please try again.\"\n },\n \"connection-lost\": {\n code: \"DATABASE_ERROR\",\n statusCode: 500,\n publicMessage: \"A temporary database error occurred. Please try again.\"\n },\n syntax: {\n code: \"INTERNAL_ERROR\",\n statusCode: 500,\n publicMessage: \"An unexpected error occurred.\"\n },\n internal: {\n code: \"INTERNAL_ERROR\",\n statusCode: 500,\n publicMessage: \"An unexpected error occurred.\"\n }\n};\nvar NEXTLY_ERROR_BRAND = Symbol.for(\"nextly/NextlyError\");\nfunction hasBrand(value) {\n if (value === null || typeof value !== \"object\" && typeof value !== \"function\") {\n return false;\n }\n return value[NEXTLY_ERROR_BRAND] === true;\n}\nvar NextlyError = class _NextlyError extends Error {\n code;\n statusCode;\n publicMessage;\n publicData;\n messageKey;\n logMessage;\n logContext;\n cause;\n timestamp;\n constructor(opts) {\n super(opts.publicMessage);\n this.name = \"NextlyError\";\n this.code = opts.code;\n this.publicMessage = opts.publicMessage;\n this.publicData = opts.publicData;\n this.messageKey = opts.messageKey;\n this.logMessage = opts.logMessage;\n this.logContext = opts.logContext;\n this.cause = opts.cause;\n this.timestamp = /* @__PURE__ */ new Date();\n this.statusCode = _NextlyError.resolveStatusCode(opts);\n Error.captureStackTrace?.(this, _NextlyError);\n }\n static {\n _NextlyError.prototype[NEXTLY_ERROR_BRAND] = true;\n }\n static resolveStatusCode(opts) {\n if (typeof opts.statusCode === \"number\") return opts.statusCode;\n if (opts.code in NEXTLY_ERROR_STATUS) {\n return NEXTLY_ERROR_STATUS[opts.code];\n }\n return 500;\n }\n /** HTTP-safe JSON. Strips logMessage / logContext / cause / stack. */\n toResponseJSON(requestId) {\n const json = {\n code: String(this.code),\n message: this.publicMessage,\n requestId\n };\n if (this.messageKey) json.messageKey = this.messageKey;\n if (this.publicData !== void 0) json.data = this.publicData;\n return json;\n }\n /** Operator-facing JSON for log lines. Includes everything. */\n toLogJSON(requestId) {\n return {\n code: this.code,\n statusCode: this.statusCode,\n publicMessage: this.publicMessage,\n messageKey: this.messageKey,\n logMessage: this.logMessage,\n logContext: this.logContext,\n cause: this.cause ? {\n name: this.cause.name,\n message: this.cause.message,\n stack: this.cause.stack\n } : void 0,\n timestamp: this.timestamp.toISOString(),\n requestId\n };\n }\n // ────────────────────────────────────────────────────────────────────\n // Static factories — the recommended throw site for every common case.\n // Public messages here follow the §13.8 rubric: complete sentence,\n // generic, no identifiers, no value echoing, no policy hints.\n // ────────────────────────────────────────────────────────────────────\n static invalidCredentials(opts) {\n return new _NextlyError({\n code: \"AUTH_INVALID_CREDENTIALS\",\n publicMessage: \"Invalid email or password.\",\n logMessage: \"Login failed\",\n logContext: opts?.logContext\n });\n }\n static authRequired(opts) {\n return new _NextlyError({\n code: \"AUTH_REQUIRED\",\n publicMessage: \"Authentication required.\",\n logContext: opts?.logContext\n });\n }\n /**\n * Distinct from `authRequired`: the caller was authenticated but the\n * session token expired. Clients key on the TOKEN_EXPIRED *code* to\n * silently refresh and retry rather than redirecting to login; the public\n * message stays the generic spec §13.6 string (\"Authentication required.\")\n * so the wire never reveals the session state — same as `authRequired`.\n */\n static tokenExpired(opts) {\n return new _NextlyError({\n code: \"TOKEN_EXPIRED\",\n publicMessage: \"Authentication required.\",\n logContext: opts?.logContext\n });\n }\n static notFound(opts) {\n return new _NextlyError({\n code: \"NOT_FOUND\",\n publicMessage: opts?.message ?? \"Not found.\",\n cause: opts?.cause,\n logContext: opts?.logContext\n });\n }\n static forbidden(opts) {\n return new _NextlyError({\n code: \"FORBIDDEN\",\n publicMessage: \"You don't have permission to perform this action.\",\n cause: opts?.cause,\n logContext: opts?.logContext\n });\n }\n /**\n * A call the caller got wrong, where naming the mistake IS the value of the\n * error.\n *\n * The only factory that takes its public message from the caller. The\n * generic messages elsewhere exist so an HTTP response cannot leak internal\n * detail; this one is for arguments and configuration a developer controls\n * and must be told about — a missing option, an unusable combination — where\n * `internal()` would reduce the one useful sentence to \"An unexpected error\n * occurred.\" Do not pass user-supplied data through it.\n */\n static invalidInput(opts) {\n return new _NextlyError({\n code: \"INVALID_INPUT\",\n publicMessage: opts.message,\n logContext: opts.logContext\n });\n }\n static validation(opts) {\n return new _NextlyError({\n code: \"VALIDATION_ERROR\",\n publicMessage: \"Validation failed.\",\n publicData: { errors: opts.errors },\n cause: opts.cause,\n logContext: opts.logContext\n });\n }\n static conflict(opts) {\n return new _NextlyError({\n code: \"CONFLICT\",\n publicMessage: opts?.message ?? \"The resource has changed since you last loaded it. Please refresh and try again.\",\n cause: opts?.cause,\n logContext: { reason: opts?.reason, ...opts?.logContext }\n });\n }\n static duplicate(opts) {\n return new _NextlyError({\n code: \"DUPLICATE\",\n publicMessage: \"Resource already exists.\",\n logContext: opts?.logContext\n });\n }\n static rateLimited(opts) {\n return new _NextlyError({\n code: \"RATE_LIMITED\",\n publicMessage: \"Too many requests. Please try again later.\",\n publicData: opts?.retryAfterSeconds !== void 0 ? { retryAfterSeconds: opts.retryAfterSeconds } : void 0,\n logContext: opts?.logContext\n });\n }\n static internal(opts) {\n return new _NextlyError({\n code: \"INTERNAL_ERROR\",\n publicMessage: \"An unexpected error occurred.\",\n cause: opts?.cause,\n logContext: opts?.logContext\n });\n }\n // Accepts an optional `logMessage` override so callers (e.g. the health\n // route) can record a specific operator narrative (\"Health check failed\")\n // while the public message stays canonical per spec §13.8.5.\n static serviceUnavailable(opts) {\n return new _NextlyError({\n code: \"SERVICE_UNAVAILABLE\",\n publicMessage: opts?.publicMessage ?? \"Service unavailable. Please try again later.\",\n logMessage: opts?.logMessage,\n cause: opts?.cause,\n logContext: opts?.logContext\n });\n }\n /**\n * Convert a DbError (or arbitrary unknown thrown by the DB layer) to a\n * NextlyError with a generic public message and rich logContext. Used by\n * `withDbErrors` for auto-conversion (Pattern A) and by services that\n * catch DB errors at boundaries (Pattern B). Spec §8.2 mapping table.\n *\n * Never leaks DB driver text, constraint names, or table names into\n * `publicMessage`. All DB context goes into `logContext`. The original\n * DbError is preserved as `cause`.\n */\n static fromDatabaseError(error) {\n if (isDbError(error)) {\n const mapping = DB_ERROR_MAPPING[error.kind];\n const logContext = {\n dbKind: error.kind,\n dialect: error.dialect\n };\n if (error.code !== void 0) logContext.dbCode = error.code;\n if (error.meta !== void 0) logContext.meta = error.meta;\n return new _NextlyError({\n code: mapping.code,\n statusCode: mapping.statusCode,\n publicMessage: mapping.publicMessage,\n logMessage: \"Database error\",\n logContext,\n cause: error\n });\n }\n return new _NextlyError({\n code: \"INTERNAL_ERROR\",\n statusCode: 500,\n publicMessage: \"An unexpected error occurred.\",\n logMessage: \"Non-DbError passed to fromDatabaseError\",\n cause: error instanceof Error ? error : void 0,\n logContext: error instanceof Error ? void 0 : { value: String(error) }\n });\n }\n // ────────────────────────────────────────────────────────────────────\n // Type guards. Structural rather than `instanceof` so they survive\n // package-boundary mismatches (one consumer's NextlyError is a\n // different module instance from another's).\n // ────────────────────────────────────────────────────────────────────\n static is(err) {\n return hasBrand(err);\n }\n static isCode(err, code) {\n return hasBrand(err) && err.code === code;\n }\n static isNotFound(err) {\n return _NextlyError.isCode(err, \"NOT_FOUND\");\n }\n static isValidation(err) {\n return _NextlyError.isCode(err, \"VALIDATION_ERROR\");\n }\n static isAuthRequired(err) {\n return _NextlyError.isCode(err, \"AUTH_REQUIRED\");\n }\n static isForbidden(err) {\n return _NextlyError.isCode(err, \"FORBIDDEN\");\n }\n static isConflict(err) {\n return _NextlyError.isCode(err, \"CONFLICT\");\n }\n static isRateLimited(err) {\n return _NextlyError.isCode(err, \"RATE_LIMITED\");\n }\n};\n\nexport {\n DbError,\n isDbError,\n toDbError,\n safeCode,\n NEXTLY_ERROR_STATUS,\n statusToErrorCode,\n genericPublicMessage,\n NextlyError\n};\n","import {\n NextlyError\n} from \"./chunk-HJ5IQPOY.mjs\";\n\n// src/storage/read-errors.ts\nvar StorageReadTooLargeError = class extends NextlyError {\n constructor(path, maxBytes, size) {\n super({\n code: \"STORAGE_READ_TOO_LARGE\",\n publicMessage: \"The stored file is larger than the limit for this read.\",\n logContext: {\n path,\n maxBytes,\n ...size === void 0 ? {} : { size }\n }\n });\n this.path = path;\n this.maxBytes = maxBytes;\n this.size = size;\n }\n};\nfunction isStorageReadTooLarge(error) {\n return NextlyError.is(error) && error.code === \"STORAGE_READ_TOO_LARGE\";\n}\nvar StorageReadTimeoutError = class extends NextlyError {\n constructor(path, timeoutMs) {\n super({\n code: \"STORAGE_READ_TIMEOUT\",\n publicMessage: \"The stored file could not be read in time.\",\n logContext: { path, timeoutMs }\n });\n this.path = path;\n this.timeoutMs = timeoutMs;\n }\n};\nfunction isStorageReadTimeout(error) {\n return NextlyError.is(error) && error.code === \"STORAGE_READ_TIMEOUT\";\n}\n\nexport {\n StorageReadTooLargeError,\n isStorageReadTooLarge,\n StorageReadTimeoutError,\n isStorageReadTimeout\n};\n","import {\n StorageReadTimeoutError,\n StorageReadTooLargeError\n} from \"./chunk-W77AQUUQ.mjs\";\nimport {\n NextlyError\n} from \"./chunk-HJ5IQPOY.mjs\";\n\n// src/utils/validate-external-url.ts\nvar IPV4_RE = /^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$/;\nvar PRIVATE_IPV4_CIDRS = [\n \"0.0.0.0/8\",\n // RFC1122 \"this network\" / wildcard\n \"10.0.0.0/8\",\n // RFC1918\n \"100.64.0.0/10\",\n // RFC6598 CGNAT\n \"127.0.0.0/8\",\n // RFC1122 loopback\n \"169.254.0.0/16\",\n // RFC3927 link-local + AWS/GCP metadata\n \"172.16.0.0/12\",\n // RFC1918\n \"192.0.0.0/24\",\n // RFC6890 IETF protocol assignments\n \"192.0.2.0/24\",\n // RFC5737 documentation\n \"192.168.0.0/16\",\n // RFC1918\n \"198.18.0.0/15\",\n // RFC2544 benchmarking\n \"198.51.100.0/24\",\n // RFC5737 documentation\n \"203.0.113.0/24\",\n // RFC5737 documentation\n \"224.0.0.0/4\",\n // RFC5771 multicast\n \"240.0.0.0/4\"\n // RFC1112 reserved\n];\nvar CLOUD_METADATA_HOSTS = /* @__PURE__ */ new Set([\n \"metadata.google.internal\",\n \"metadata.googleapis.com\",\n \"metadata\"\n // GCP short form\n]);\nvar ExternalUrlError = class extends NextlyError {\n constructor(message, url) {\n super({\n code: \"EXTERNAL_URL_BLOCKED\",\n publicMessage: message,\n logContext: { url }\n });\n this.url = url;\n this.name = \"ExternalUrlError\";\n }\n};\nasync function validateExternalUrl(rawUrl, options = {}) {\n let parsed;\n try {\n parsed = new URL(rawUrl);\n } catch {\n throw new ExternalUrlError(\"Invalid URL\", rawUrl);\n }\n const hostname = parsed.hostname.toLowerCase().replace(/^\\[|\\]$/g, \"\");\n const isLocalhostHostname = hostname === \"localhost\" || hostname === \"127.0.0.1\" || hostname === \"::1\";\n const baseProtocols = options.allowedProtocols ?? [\"https:\"];\n const protocols = options.allowLocalhost && isLocalhostHostname ? [...baseProtocols, \"http:\"] : baseProtocols;\n if (!protocols.includes(parsed.protocol)) {\n throw new ExternalUrlError(\n `Protocol ${parsed.protocol} not allowed (allowed: ${protocols.join(\", \")})`,\n rawUrl\n );\n }\n if (CLOUD_METADATA_HOSTS.has(hostname)) {\n throw new ExternalUrlError(\n `Cloud-metadata hostname rejected: ${parsed.hostname}`,\n rawUrl\n );\n }\n if (IPV4_RE.test(hostname)) {\n if (!isPublicIpv4(\n hostname,\n options.allowLocalhost === true && hostname === \"127.0.0.1\"\n )) {\n throw new ExternalUrlError(\n `Resolved to non-public IP: ${hostname}`,\n rawUrl\n );\n }\n return { url: parsed, pinnedIp: hostname, family: 4 };\n }\n if (hostname.includes(\":\")) {\n if (!isPublicIpv6(\n hostname,\n options.allowLocalhost === true && hostname === \"::1\"\n )) {\n throw new ExternalUrlError(\n `Resolved to non-public IP: ${hostname}`,\n rawUrl\n );\n }\n return { url: parsed, pinnedIp: hostname, family: 6 };\n }\n const { lookup } = await import(\"dns/promises\");\n let addresses;\n try {\n addresses = await lookup(hostname, { all: true });\n } catch (err) {\n throw new ExternalUrlError(\n `DNS lookup failed: ${err.message}`,\n rawUrl\n );\n }\n if (addresses.length === 0) {\n throw new ExternalUrlError(\"No IPs returned for host\", rawUrl);\n }\n for (const { address, family } of addresses) {\n const allow = options.allowLocalhost === true && isLocalhostHostname && family === 4;\n const ok = family === 4 ? isPublicIpv4(address, allow) : isPublicIpv6(address, allow);\n if (!ok) {\n throw new ExternalUrlError(\n `Resolved to non-public IP: ${address}`,\n rawUrl\n );\n }\n }\n const first = addresses[0];\n return {\n url: parsed,\n pinnedIp: first.address,\n family: first.family === 6 ? 6 : 4\n };\n}\nvar DEFAULT_MAX_RESPONSE_BYTES = 10 * 1024 * 1024;\nvar DEFAULT_TIMEOUT_MS = 3e4;\nvar SafeFetchError = class extends NextlyError {\n constructor(message, url, reason, status) {\n super({\n code: \"EXTERNAL_REQUEST_FAILED\",\n publicMessage: message,\n logContext: { url, reason }\n });\n this.url = url;\n this.reason = reason;\n this.status = status;\n this.name = \"SafeFetchError\";\n }\n};\nfunction createPinnedLookup(ip, family) {\n return (_hostname, options, callback) => {\n if (options && options.all) {\n callback(null, [{ address: ip, family }]);\n } else {\n callback(null, ip, family);\n }\n };\n}\nasync function safeFetch(rawUrl, options = {}) {\n const {\n allowLocalhost,\n allowedProtocols,\n method,\n headers,\n body,\n signal,\n maxResponseBytes = DEFAULT_MAX_RESPONSE_BYTES,\n timeoutMs = DEFAULT_TIMEOUT_MS\n } = options;\n const controller = new AbortController();\n const forwardAbort = () => controller.abort(signal?.reason);\n if (signal) {\n if (signal.aborted) controller.abort(signal.reason);\n else signal.addEventListener(\"abort\", forwardAbort, { once: true });\n }\n const timer = setTimeout(\n () => controller.abort(\n new SafeFetchError(`Request exceeded ${timeoutMs}ms`, rawUrl, \"timeout\")\n ),\n timeoutMs\n );\n try {\n const validated = await abortable(\n validateExternalUrl(rawUrl, { allowLocalhost, allowedProtocols }),\n controller.signal\n );\n return await pinnedFetch(validated, {\n method,\n headers,\n body,\n signal: controller.signal,\n maxResponseBytes\n });\n } finally {\n clearTimeout(timer);\n if (signal) signal.removeEventListener(\"abort\", forwardAbort);\n }\n}\nfunction abortError(signal) {\n const reason = signal.reason;\n if (reason instanceof Error) return reason;\n return new DOMException(\"The operation was aborted\", \"AbortError\");\n}\nfunction abortable(promise, signal) {\n if (signal.aborted) return Promise.reject(abortError(signal));\n let onAbort;\n const aborted = new Promise((_resolve, reject) => {\n onAbort = () => reject(abortError(signal));\n signal.addEventListener(\"abort\", onAbort, { once: true });\n });\n return Promise.race([promise, aborted]).finally(\n () => signal.removeEventListener(\"abort\", onAbort)\n );\n}\nasync function pinnedFetch(validated, init) {\n const { url, pinnedIp, family } = validated;\n const httpMod = url.protocol === \"https:\" ? await import(\"https\") : await import(\"http\");\n const zlib = await import(\"zlib\");\n const lookup = createPinnedLookup(pinnedIp, family);\n const outHeaders = toOutgoingHeaders(init.headers);\n if (init.body != null) {\n delete outHeaders[\"transfer-encoding\"];\n outHeaders[\"content-length\"] = String(\n typeof init.body === \"string\" ? Buffer.byteLength(init.body) : init.body.byteLength\n );\n }\n return new Promise((resolve, reject) => {\n let settled = false;\n const settle = (fn) => {\n if (settled) return;\n settled = true;\n fn();\n };\n const failure = (err) => init.signal.aborted ? abortError(init.signal) : err;\n const req = httpMod.request(\n url,\n {\n method: init.method ?? \"GET\",\n headers: outHeaders,\n lookup,\n // Fresh socket per request so a pooled connection can't reuse a\n // differently-resolved address, and so the pinned lookup always runs.\n agent: false,\n // The combined deadline/caller signal aborts the request in flight.\n signal: init.signal\n },\n (res) => {\n const chunks = [];\n let received = 0;\n res.on(\"data\", (chunk) => {\n received += chunk.length;\n if (received > init.maxResponseBytes) {\n res.destroy();\n req.destroy();\n settle(\n () => reject(\n new SafeFetchError(\n `Response body exceeded ${init.maxResponseBytes} bytes`,\n url.href,\n \"response-too-large\",\n res.statusCode\n )\n )\n );\n return;\n }\n chunks.push(chunk);\n });\n res.on(\"end\", () => {\n const decoded = decodeBody(\n Buffer.concat(chunks),\n res.headers[\"content-encoding\"],\n zlib,\n init.maxResponseBytes,\n url.href,\n res.statusCode\n );\n if (decoded instanceof SafeFetchError) {\n settle(() => reject(decoded));\n return;\n }\n settle(\n () => resolve(toWhatwgResponse(res, decoded.body, decoded.decoded))\n );\n });\n res.on(\"error\", (err) => settle(() => reject(failure(err))));\n }\n );\n req.on(\"error\", (err) => settle(() => reject(failure(err))));\n if (!req.destroyed) {\n if (init.body != null) req.write(init.body);\n req.end();\n }\n });\n}\nfunction toOutgoingHeaders(headers) {\n const out = {};\n if (!headers) return out;\n const add = (rawKey, value) => {\n const key = rawKey.toLowerCase();\n if (key === \"host\") return;\n const existing = out[key];\n out[key] = existing === void 0 ? value : [].concat(existing, value);\n };\n if (headers instanceof Headers) {\n headers.forEach((value, key) => add(key, value));\n } else if (Array.isArray(headers)) {\n for (const [key, value] of headers) add(key, value);\n } else {\n for (const [key, value] of Object.entries(headers)) add(key, value);\n }\n return out;\n}\nfunction toWhatwgResponse(res, body, stripContentHeaders) {\n const headers = new Headers();\n for (const [key, value] of Object.entries(res.headers)) {\n if (value == null) continue;\n const lower = key.toLowerCase();\n if (stripContentHeaders && (lower === \"content-encoding\" || lower === \"content-length\")) {\n continue;\n }\n const values = Array.isArray(value) ? value : [value];\n for (const v of values) {\n try {\n headers.append(key, v);\n } catch {\n }\n }\n }\n const raw = res.statusCode ?? 502;\n const status = raw >= 200 && raw <= 599 ? raw : 502;\n const nullBody = status === 204 || status === 205 || status === 304;\n const bytes = new Uint8Array(body.byteLength);\n bytes.set(body);\n return new Response(nullBody ? null : bytes, {\n status,\n statusText: res.statusMessage ?? \"\",\n headers\n });\n}\nfunction decodeBody(buf, encoding, zlib, cap, url, status) {\n if (!encoding) return { body: buf, decoded: false };\n if (buf.length === 0) return { body: buf, decoded: true };\n const layers = encoding.split(\",\").map((e) => e.trim().toLowerCase()).filter(Boolean);\n const opts = { maxOutputLength: cap };\n let current = buf;\n let decoded = false;\n try {\n for (let i = layers.length - 1; i >= 0; i--) {\n const enc = layers[i];\n if (enc === \"identity\") continue;\n if (enc === \"gzip\" || enc === \"x-gzip\") {\n current = zlib.gunzipSync(current, opts);\n } else if (enc === \"br\") {\n current = zlib.brotliDecompressSync(current, opts);\n } else if (enc === \"deflate\") {\n try {\n current = zlib.inflateSync(current, opts);\n } catch (inflateErr) {\n if (errorCode(inflateErr) === \"ERR_BUFFER_TOO_LARGE\")\n throw inflateErr;\n current = zlib.inflateRawSync(current, opts);\n }\n } else {\n if (decoded) {\n return new SafeFetchError(\n `Cannot fully decode content-encoding \"${encoding}\"`,\n url,\n \"decode-failed\",\n status\n );\n }\n return { body: buf, decoded: false };\n }\n decoded = true;\n }\n return { body: current, decoded };\n } catch (err) {\n const tooLarge = errorCode(err) === \"ERR_BUFFER_TOO_LARGE\";\n return new SafeFetchError(\n tooLarge ? `Decoded ${encoding} response body exceeded the size cap` : `Failed to decode ${encoding} response body`,\n url,\n tooLarge ? \"response-too-large\" : \"decode-failed\",\n status\n );\n }\n}\nfunction errorCode(err) {\n if (err != null && typeof err === \"object\" && \"code\" in err) {\n const code = err.code;\n return typeof code === \"string\" ? code : void 0;\n }\n return void 0;\n}\nfunction ipv4ToInt(ip) {\n const parts = ip.split(\".\");\n if (parts.length !== 4) return null;\n let acc = 0;\n for (const p of parts) {\n const n = Number(p);\n if (!Number.isInteger(n) || n < 0 || n > 255) return null;\n acc = acc << 8 | n;\n }\n return acc >>> 0;\n}\nfunction isIpv4InCidr(intIp, cidr) {\n const [addr, prefixRaw] = cidr.split(\"/\");\n const intCidr = ipv4ToInt(addr);\n if (intCidr === null) return false;\n const prefix = Number(prefixRaw);\n if (!Number.isInteger(prefix) || prefix < 0 || prefix > 32) return false;\n const mask = prefix === 0 ? 0 : -1 >>> 32 - prefix << 32 - prefix;\n return (intIp & mask) >>> 0 === (intCidr & mask) >>> 0;\n}\nfunction isPublicIpv4(addr, allowLoopback) {\n const intIp = ipv4ToInt(addr);\n if (intIp === null) return false;\n if (allowLoopback && addr === \"127.0.0.1\") return true;\n for (const cidr of PRIVATE_IPV4_CIDRS) {\n if (isIpv4InCidr(intIp, cidr)) return false;\n }\n return true;\n}\nfunction mappedIpv4(lower) {\n const dotted = lower.match(/^::ffff:(\\d{1,3}\\.\\d{1,3}\\.\\d{1,3}\\.\\d{1,3})$/);\n if (dotted) return dotted[1];\n const hex = lower.match(/^::ffff:([0-9a-f]{1,4}):([0-9a-f]{1,4})$/);\n if (hex) {\n const hi = parseInt(hex[1], 16);\n const lo = parseInt(hex[2], 16);\n return [hi >> 8, hi & 255, lo >> 8, lo & 255].join(\".\");\n }\n return null;\n}\nfunction isPublicIpv6(addr, allowLoopback) {\n const lower = addr.toLowerCase();\n if (allowLoopback && lower === \"::1\") return true;\n const mapped = mappedIpv4(lower);\n if (mapped) {\n return isPublicIpv4(mapped, allowLoopback);\n }\n if (lower === \"::\" || lower === \"::1\") return false;\n const firstHextet = lower.split(\":\")[0] || \"\";\n if (firstHextet.startsWith(\"fc\") || firstHextet.startsWith(\"fd\"))\n return false;\n if (firstHextet.startsWith(\"fe8\") || firstHextet.startsWith(\"fe9\"))\n return false;\n if (firstHextet.startsWith(\"fea\") || firstHextet.startsWith(\"feb\"))\n return false;\n if (firstHextet.startsWith(\"ff\")) return false;\n return true;\n}\n\n// src/storage/fetch-stored-bytes.ts\nasync function fetchStoredBytes(url, context, label, options, signal) {\n let response;\n try {\n response = await safeFetch(url, {\n ...options?.maxBytes === void 0 ? {} : { maxResponseBytes: options.maxBytes },\n /*\n * A caller's signal REPLACES the deadline rather than joining it.\n *\n * These adapters look the object's address up before fetching it, and\n * that lookup can stall too. Starting a fresh timer here would give the\n * fetch its own full budget on top of however long the lookup took, so a\n * read could outlive the deadline the caller was told applied — by\n * roughly double. One signal begun before the lookup governs both phases.\n */\n ...signal !== void 0 ? { signal } : options?.timeoutMs === void 0 ? {} : { timeoutMs: options.timeoutMs }\n });\n } catch (error) {\n const verdict = classifyFetchFailure(error);\n if (verdict === \"absent\") return null;\n if (verdict === \"oversized\") {\n throw new StorageReadTooLargeError(\n context,\n resolveReadBounds(options).maxBytes\n );\n }\n throw error;\n }\n if (response.status === 404) return null;\n if (!response.ok) {\n throw NextlyError.internal({\n logContext: {\n service: label,\n path: context,\n status: response.status,\n statusText: response.statusText\n }\n });\n }\n return Buffer.from(await response.arrayBuffer());\n}\nfunction classifyFetchFailure(error) {\n if (!(error instanceof SafeFetchError)) return \"unknown\";\n if (error.status === 404) return \"absent\";\n const successful = error.status !== void 0 && error.status >= 200 && error.status < 300;\n return error.reason === \"response-too-large\" && successful ? \"oversized\" : \"unknown\";\n}\nvar DEFAULT_READ_TIMEOUT_MS = DEFAULT_TIMEOUT_MS;\nvar DEFAULT_READ_MAX_BYTES = DEFAULT_MAX_RESPONSE_BYTES;\nfunction resolveReadBounds(options) {\n return {\n maxBytes: options?.maxBytes ?? DEFAULT_READ_MAX_BYTES,\n timeoutMs: options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS\n };\n}\nfunction deadlineSignal(timeoutMs, context) {\n const controller = new AbortController();\n const timer = setTimeout(() => {\n controller.abort(new StorageReadTimeoutError(context, timeoutMs));\n }, timeoutMs);\n timer.unref?.();\n return {\n signal: controller.signal,\n /*\n * Called once the read has settled, whichever way it went. `unref` only\n * stops a pending timer holding the process open; it still sits on the\n * timer heap retaining the controller and the path, so a server doing a\n * thousand reads a second keeps thirty seconds of them alive and then runs\n * a thousand aborts that answer nobody.\n */\n cancel: () => {\n clearTimeout(timer);\n }\n };\n}\nasync function withDeadline(work, signal) {\n if (signal === void 0) return await work;\n return await Promise.race([\n work,\n new Promise((_, reject) => {\n if (signal.aborted) {\n reject(signal.reason);\n return;\n }\n signal.addEventListener(\"abort\", () => reject(signal.reason), {\n once: true\n });\n })\n ]);\n}\n\nexport {\n ExternalUrlError,\n validateExternalUrl,\n SafeFetchError,\n safeFetch,\n fetchStoredBytes,\n classifyFetchFailure,\n DEFAULT_READ_TIMEOUT_MS,\n DEFAULT_READ_MAX_BYTES,\n resolveReadBounds,\n deadlineSignal,\n withDeadline\n};\n","import {\n deadlineSignal,\n resolveReadBounds,\n withDeadline\n} from \"./chunk-C2VHLQKK.mjs\";\nimport {\n StorageReadTooLargeError\n} from \"./chunk-W77AQUUQ.mjs\";\n\n// src/storage/adapters/local-adapter.ts\nimport * as fs from \"fs/promises\";\nimport * as path from \"path\";\n\n// src/storage/adapters/base-adapter.ts\nvar BaseStorageAdapter = class {\n /**\n * Get adapter info including capabilities.\n *\n * Default implementation that auto-detects capabilities by checking\n * if getSignedUrl and getPresignedUploadUrl methods are implemented.\n * Override in subclasses for more accurate capability reporting.\n *\n * @returns Adapter info with type, name, and capability flags\n */\n getInfo() {\n const hasSignedUrls = \"getSignedUrl\" in this && typeof this.getSignedUrl === \"function\";\n const hasClientUploads = \"getPresignedUploadUrl\" in this && typeof this.getPresignedUploadUrl === \"function\";\n return {\n type: this.getType(),\n name: this.constructor.name,\n supportsSignedUrls: hasSignedUrls,\n supportsClientUploads: hasClientUploads\n };\n }\n /**\n * Sanitize filename to prevent directory traversal and storage issues.\n *\n * Security measures:\n * - Remove path separators (/, \\)\n * - Keep only basename (no directories)\n * - Replace problematic characters with hyphens\n * - Preserve alphanumeric, dots, underscores, hyphens\n *\n * @param filename - Original filename to sanitize\n * @returns Sanitized filename safe for storage\n *\n * @example\n * ```typescript\n * this.sanitizeFilename('../../../etc/passwd') // 'passwd'\n * this.sanitizeFilename('my file (1).jpg') // 'my-file--1-.jpg'\n * this.sanitizeFilename('photo.jpg') // 'photo.jpg'\n * ```\n */\n sanitizeFilename(filename) {\n const basename = filename.split(/[/\\\\]/).pop() || filename;\n return basename.replace(/[^a-zA-Z0-9._-]/g, \"-\");\n }\n /**\n * Generate a unique storage key with date-based prefix.\n *\n * Creates keys in format: {folder}/{year}/{month}/{uuid}-{sanitized-filename}\n * This provides:\n * - Unique keys via UUID to prevent collisions\n * - Date-based organization for easier management\n * - Readable filenames for debugging\n *\n * @param filename - Original filename (will be sanitized)\n * @param folder - Optional folder/prefix for organizing uploads\n * @returns Generated storage key\n *\n * @example\n * ```typescript\n * this.generateKey('photo.jpg')\n * // '2026/01/abc-123-...-photo.jpg'\n *\n * this.generateKey('doc.pdf', 'documents')\n * // 'documents/2026/01/abc-123-...-doc.pdf'\n * ```\n */\n generateKey(filename, folder) {\n const sanitized = this.sanitizeFilename(filename);\n const uuid = crypto.randomUUID();\n const date = /* @__PURE__ */ new Date();\n const year = date.getFullYear();\n const month = String(date.getMonth() + 1).padStart(2, \"0\");\n const prefix = folder ? `${folder}/${year}/${month}` : `${year}/${month}`;\n return `${prefix}/${uuid}-${sanitized}`;\n }\n};\n\n// src/storage/adapters/local-adapter.ts\nvar gitignoreUpdated = false;\nvar READ_CHUNK_BYTES = 64 * 1024;\nasync function readCounted(handle, filePath, maxBytes, signal) {\n const chunks = [];\n const chunk = Buffer.allocUnsafe(READ_CHUNK_BYTES);\n let received = 0;\n for (; ; ) {\n if (signal.aborted) throw signal.reason;\n const { bytesRead } = await handle.read(chunk, 0, READ_CHUNK_BYTES, null);\n if (bytesRead === 0) break;\n received += bytesRead;\n if (received > maxBytes) {\n throw new StorageReadTooLargeError(filePath, maxBytes, received);\n }\n chunks.push(Buffer.from(chunk.subarray(0, bytesRead)));\n }\n return Buffer.concat(chunks);\n}\nvar LocalStorageAdapter = class extends BaseStorageAdapter {\n basePath;\n baseUrl;\n constructor(config) {\n super();\n this.basePath = path.resolve(config.basePath);\n this.baseUrl = config.baseUrl.replace(/\\/+$/, \"\");\n }\n /**\n * Upload file to local disk.\n * Creates directories as needed and writes the file buffer.\n */\n async upload(buffer, options) {\n const key = this.generateKey(options.filename, options.folder);\n const fullPath = this.resolveAndValidate(key);\n await fs.mkdir(path.dirname(fullPath), { recursive: true });\n await fs.writeFile(fullPath, buffer);\n await this.ensureGitignore();\n return {\n url: this.getPublicUrl(key),\n path: key\n };\n }\n /**\n * Delete file from local disk.\n * Silently succeeds if the file doesn't exist.\n */\n async delete(filePath) {\n let fullPath;\n try {\n fullPath = this.resolveAndValidate(filePath);\n } catch {\n return;\n }\n try {\n await fs.unlink(fullPath);\n } catch (err) {\n if (err.code !== \"ENOENT\") {\n throw err;\n }\n }\n }\n /**\n * Bulk delete files from local disk.\n * Uses parallel unlinks with Promise.allSettled for best performance.\n */\n async bulkDelete(filePaths) {\n const results = await Promise.allSettled(\n filePaths.map(async (filePath) => {\n await this.delete(filePath);\n return filePath;\n })\n );\n const successful = [];\n const failed = [];\n results.forEach((result, index) => {\n if (result.status === \"fulfilled\") {\n successful.push(filePaths[index]);\n } else {\n failed.push({\n filePath: filePaths[index],\n error: result.reason?.message || \"Unknown error\"\n });\n }\n });\n return { successful, failed };\n }\n /**\n * Check if file exists on local disk.\n */\n async exists(filePath) {\n try {\n const fullPath = this.resolveAndValidate(filePath);\n await fs.access(fullPath);\n return true;\n } catch {\n return false;\n }\n }\n /**\n * Get public URL for a file.\n * Returns baseUrl + relative path for Next.js static file serving.\n */\n getPublicUrl(filePath) {\n const cleanPath = filePath.replace(/^\\/+/, \"\");\n return `${this.baseUrl}/${cleanPath}`;\n }\n /**\n * Get storage type identifier.\n */\n getType() {\n return \"local\";\n }\n /**\n * Read file contents from local disk.\n *\n * Returns the file buffer, or `null` if the file is not found.\n *\n * Honours the caller's bounds, which matters MORE here than anywhere else:\n * this is the default backend, so a bound the cloud adapters keep and this\n * one ignores is a bound that does nothing in the commonest deployment.\n *\n * Both bounds are enforced against ONE open descriptor. Resolving the name,\n * asking for its size and then reading it again by name resolves the same\n * name three times: a file replaced in between is read under a cap measured\n * on the file it displaced, and one appended to in between is buffered whole\n * however small it was when asked. The descriptor settles the first, and\n * counting the bytes as they arrive settles the second.\n */\n async read(filePath, options) {\n let fullPath;\n try {\n fullPath = this.resolveAndValidate(filePath);\n } catch {\n return null;\n }\n const bounds = resolveReadBounds(options);\n const deadline = deadlineSignal(bounds.timeoutMs, filePath);\n const work = this.readWithinCap(\n fullPath,\n filePath,\n bounds.maxBytes,\n deadline.signal\n );\n void work.catch(() => void 0);\n try {\n return await withDeadline(work, deadline.signal);\n } finally {\n deadline.cancel();\n }\n }\n // ============================================================\n // Private Helpers\n // ============================================================\n /**\n * Read one opened file, refusing it the moment it exceeds the cap.\n *\n * Split out so the whole descriptor lifetime — open, size, read, close —\n * sits inside a single promise the caller can race, and so the close in its\n * `finally` still runs when that race has already answered the caller.\n *\n * @param fullPath - Absolute path, already validated against `basePath`\n * @param filePath - The caller's path, carried only in the refusal\n * @param maxBytes - The cap this read runs under\n */\n async readWithinCap(fullPath, filePath, maxBytes, signal) {\n const handle = await fs.open(fullPath, \"r\").catch(() => null);\n if (handle === null) return null;\n try {\n if (signal.aborted) throw signal.reason;\n const { size } = await handle.stat();\n if (size > maxBytes) {\n throw new StorageReadTooLargeError(filePath, maxBytes, size);\n }\n return await readCounted(handle, filePath, maxBytes, signal);\n } catch (error) {\n if (error instanceof StorageReadTooLargeError) throw error;\n return null;\n } finally {\n await handle.close().catch(() => void 0);\n }\n }\n /**\n * Resolve a relative file path to an absolute path within basePath.\n * Throws if the resolved path would escape basePath (path traversal attack).\n */\n resolveAndValidate(filePath) {\n const sanitized = filePath.replace(/^[/\\\\]+/, \"\").replace(/\\.\\.[/\\\\]/g, \"\");\n const fullPath = path.resolve(this.basePath, sanitized);\n if (!fullPath.startsWith(this.basePath)) {\n throw new Error(\n `Path traversal detected: ${filePath} resolves outside of storage directory`\n );\n }\n return fullPath;\n }\n /**\n * Auto-add the uploads directory to .gitignore on first upload.\n * Prevents accidentally committing uploaded files to git.\n */\n async ensureGitignore() {\n if (gitignoreUpdated) return;\n gitignoreUpdated = true;\n try {\n const projectRoot = path.resolve(this.basePath, \"..\", \"..\");\n const gitignorePath = path.join(projectRoot, \".gitignore\");\n let content = \"\";\n try {\n content = await fs.readFile(gitignorePath, \"utf-8\");\n } catch {\n }\n const uploadsDirRelative = path.relative(projectRoot, this.basePath);\n const ignorePattern = uploadsDirRelative + \"/\";\n if (!content.includes(ignorePattern)) {\n const newEntry = `\n# Nextly local uploads (auto-added)\n${ignorePattern}\n`;\n await fs.writeFile(gitignorePath, content + newEntry, \"utf-8\");\n }\n } catch {\n }\n }\n};\n\n// src/storage/adapters/local-plugin.ts\nfunction localStorage(config) {\n if (config.enabled === false) {\n return {\n name: \"local-storage\",\n type: \"local\",\n collections: {},\n adapter: null\n };\n }\n const adapter = new LocalStorageAdapter({\n basePath: config.basePath ?? \"./public/uploads\",\n baseUrl: config.baseUrl ?? \"/uploads\"\n });\n return {\n name: \"local-storage\",\n type: \"local\",\n collections: config.collections,\n adapter\n // Local storage doesn't support presigned URLs or signed downloads\n };\n}\n\nexport {\n BaseStorageAdapter,\n LocalStorageAdapter,\n localStorage\n};\n","var __defProp = Object.defineProperty;\nvar __export = (target, all) => {\n for (var name in all)\n __defProp(target, name, { get: all[name], enumerable: true });\n};\n\nexport {\n __export\n};\n","import {\n LocalStorageAdapter\n} from \"./chunk-IJCL4XOT.mjs\";\n\n// src/storage/storage.ts\nvar MediaStorage = class {\n /** Registered storage plugins by name */\n plugins = /* @__PURE__ */ new Map();\n /** Storage adapter per collection */\n collectionAdapters = /* @__PURE__ */ new Map();\n /** Storage configuration per collection */\n collectionConfigs = /* @__PURE__ */ new Map();\n /** Local storage adapter (always available as fallback) */\n localAdapter;\n /**\n * Create a new MediaStorage instance.\n *\n * @param config - Optional configuration for storage initialization\n */\n constructor(config) {\n this.localAdapter = new LocalStorageAdapter({\n basePath: config?.local?.uploadDir ?? \"./public/uploads\",\n baseUrl: config?.local?.publicPath ?? \"/uploads\"\n });\n if (config?.plugins) {\n for (const plugin of config.plugins) {\n this.registerPlugin(plugin);\n }\n }\n }\n // ============================================================\n // Plugin Registration\n // ============================================================\n /**\n * Register a storage plugin.\n *\n * Plugins provide storage adapters for specific collections.\n * When a collection is registered with a plugin, uploads for that\n * collection will be routed to the plugin's adapter.\n *\n * @param plugin - The storage plugin to register\n *\n * @example\n * ```typescript\n * const storage = new MediaStorage();\n *\n * storage.registerPlugin(s3Storage({\n * bucket: 'my-bucket',\n * region: 'us-east-1',\n * collections: {\n * media: true,\n * 'private-docs': { prefix: 'private/' }\n * }\n * }));\n * ```\n */\n registerPlugin(plugin) {\n if (!plugin.adapter) {\n return;\n }\n this.plugins.set(plugin.name, plugin);\n for (const [collectionSlug, config] of Object.entries(plugin.collections)) {\n const collectionConfig = typeof config === \"boolean\" ? {} : config;\n this.collectionAdapters.set(collectionSlug, plugin.adapter);\n this.collectionConfigs.set(collectionSlug, collectionConfig);\n }\n }\n // ============================================================\n // Adapter Resolution\n // ============================================================\n /**\n * Check if any storage adapter is configured.\n *\n * @returns True if at least one storage plugin is registered\n */\n hasAdapter() {\n return true;\n }\n /**\n * Get the storage adapter if available, or null if not configured.\n *\n * Unlike getAdapter(), this method does not throw an error if no storage\n * is configured. Useful for optional storage scenarios.\n *\n * @param collection - The collection slug (optional)\n * @returns The storage adapter instance, or null if not configured\n */\n getAdapterOrNull(collection) {\n if (collection && this.collectionAdapters.has(collection)) {\n return this.collectionAdapters.get(collection);\n }\n return this.localAdapter;\n }\n /**\n * Get the storage adapter for a specific collection.\n *\n * If a plugin is configured for the collection, returns the plugin's adapter.\n * Otherwise, returns the default adapter (first registered plugin).\n *\n * @param collection - The collection slug (optional)\n * @returns The appropriate storage adapter\n * @throws Error if no storage plugin is configured\n */\n getAdapterForCollection(collection) {\n if (collection && this.collectionAdapters.has(collection)) {\n return this.collectionAdapters.get(collection);\n }\n return this.localAdapter;\n }\n /**\n * Get configuration for a specific collection.\n *\n * @param collection - The collection slug\n * @returns The collection's storage configuration, or undefined\n */\n getCollectionConfig(collection) {\n return this.collectionConfigs.get(collection);\n }\n // ============================================================\n // Core Storage Operations\n // ============================================================\n /**\n * Upload file to appropriate storage based on collection.\n *\n * Routes the upload to the correct adapter based on collection\n * configuration. Applies collection-specific prefix if configured.\n *\n * @param buffer - The file buffer to upload\n * @param options - Upload options including filename, mimeType, collection\n * @returns Upload result with URL and path\n *\n * @example\n * ```typescript\n * const result = await storage.upload(buffer, {\n * filename: 'photo.jpg',\n * mimeType: 'image/jpeg',\n * collection: 'media'\n * });\n * console.log(result.url); // Public URL\n * console.log(result.path); // Storage path for deletion\n * ```\n */\n async upload(buffer, options) {\n const adapter = this.getAdapterForCollection(options.collection);\n const config = options.collection ? this.getCollectionConfig(options.collection) : void 0;\n const uploadOptions = { ...options };\n if (config?.prefix) {\n uploadOptions.folder = config.prefix + (options.folder || \"\");\n }\n return adapter.upload(buffer, uploadOptions);\n }\n /**\n * Delete file from storage.\n *\n * Determines correct adapter based on collection.\n *\n * @param filePath - The storage path/key of the file\n * @param collection - The collection slug (optional, for routing)\n */\n async delete(filePath, collection) {\n const adapter = this.getAdapterForCollection(collection);\n return adapter.delete(filePath);\n }\n /**\n * Bulk delete files from storage.\n * Uses adapter's native bulkDelete if available, otherwise falls back to\n * sequential individual deletes in chunks of 10.\n */\n async bulkDelete(filePaths, collection) {\n const adapter = this.getAdapterForCollection(collection);\n if (adapter.bulkDelete) {\n return adapter.bulkDelete(filePaths);\n }\n const successful = [];\n const failed = [];\n const chunkSize = 10;\n for (let i = 0; i < filePaths.length; i += chunkSize) {\n const chunk = filePaths.slice(i, i + chunkSize);\n const results = await Promise.allSettled(\n chunk.map((fp) => adapter.delete(fp))\n );\n results.forEach((result, idx) => {\n const fp = chunk[idx];\n if (result.status === \"fulfilled\") {\n successful.push(fp);\n } else {\n failed.push({\n filePath: fp,\n error: result.reason instanceof Error ? result.reason.message : String(result.reason)\n });\n }\n });\n }\n return { successful, failed };\n }\n /**\n * Check if file exists in storage.\n *\n * @param filePath - The storage path/key to check\n * @param collection - The collection slug (optional, for routing)\n * @returns True if file exists\n */\n async exists(filePath, collection) {\n const adapter = this.getAdapterForCollection(collection);\n return adapter.exists(filePath);\n }\n /**\n * Get public URL for file.\n *\n * @param filePath - The storage path/key\n * @param collection - The collection slug (optional, for routing)\n * @returns Public URL to access the file\n */\n getPublicUrl(filePath, collection) {\n const adapter = this.getAdapterForCollection(collection);\n return adapter.getPublicUrl(filePath);\n }\n /**\n * Get storage type for a collection.\n *\n * @param collection - The collection slug (optional)\n * @returns Storage type identifier ('s3', 'vercel-blob')\n */\n getStorageType(collection) {\n const adapter = this.getAdapterForCollection(collection);\n return adapter.getType();\n }\n // ============================================================\n // Client Upload Support\n // ============================================================\n /**\n * Check if collection supports client-side uploads.\n *\n * Client-side uploads allow direct-to-storage uploads, bypassing\n * the server. This is essential for serverless platforms with\n * request body size limits (e.g., Vercel's 4.5MB limit).\n *\n * @param collection - The collection slug\n * @returns True if client uploads are enabled and supported\n */\n supportsClientUploads(collection) {\n const config = this.getCollectionConfig(collection);\n if (!config?.clientUploads) return false;\n const adapter = this.getAdapterForCollection(collection);\n const info = adapter.getInfo?.();\n return info?.supportsClientUploads ?? false;\n }\n /**\n * Get client upload URL for direct-to-storage uploads.\n *\n * Generates a pre-signed URL that allows the client to upload\n * directly to the storage backend, bypassing the server.\n *\n * Only available if:\n * 1. Collection is configured with `clientUploads: true`\n * 2. The storage adapter supports client uploads\n *\n * @param filename - Original filename\n * @param mimeType - File MIME type\n * @param collection - Collection slug\n * @returns Client upload data, or null if not supported\n *\n * @example\n * ```typescript\n * // Server-side: generate upload URL\n * const uploadData = await storage.getClientUploadUrl(\n * 'photo.jpg',\n * 'image/jpeg',\n * 'media'\n * );\n *\n * // Client-side: upload directly to storage\n * await fetch(uploadData.uploadUrl, {\n * method: uploadData.method,\n * headers: uploadData.headers,\n * body: file\n * });\n * ```\n */\n async getClientUploadUrl(filename, mimeType, collection) {\n if (!this.supportsClientUploads(collection)) {\n return null;\n }\n for (const plugin of this.plugins.values()) {\n if (collection in plugin.collections && plugin.getClientUploadUrl) {\n return plugin.getClientUploadUrl(filename, mimeType, collection);\n }\n }\n return null;\n }\n // ============================================================\n // Signed Download Support\n // ============================================================\n /**\n * Check if collection supports signed download URLs.\n *\n * @param collection - The collection slug\n * @returns True if signed downloads are enabled and supported\n */\n supportsSignedDownloads(collection) {\n const config = this.getCollectionConfig(collection);\n if (!config?.signedDownloads) return false;\n const adapter = this.getAdapterForCollection(collection);\n const info = adapter.getInfo?.();\n return info?.supportsSignedUrls ?? false;\n }\n /**\n * Get signed download URL for secure file access.\n *\n * Generates a time-limited signed URL for accessing files in\n * private storage buckets. Only works if:\n * 1. Collection is configured with `signedDownloads: true`\n * 2. The storage adapter supports signed URLs\n *\n * @param filePath - Storage path/key of the file\n * @param collection - Collection slug\n * @param expiresIn - URL expiry time in seconds (optional)\n * @returns Signed URL, or null if not supported\n *\n * @example\n * ```typescript\n * const signedUrl = await storage.getSignedDownloadUrl(\n * 'private/doc.pdf',\n * 'private-docs',\n * 3600 // 1 hour\n * );\n * ```\n */\n async getSignedDownloadUrl(filePath, collection, expiresIn) {\n if (!this.supportsSignedDownloads(collection)) {\n return null;\n }\n const config = this.getCollectionConfig(collection);\n for (const plugin of this.plugins.values()) {\n if (collection in plugin.collections && plugin.getSignedDownloadUrl) {\n return plugin.getSignedDownloadUrl(\n filePath,\n expiresIn ?? config?.signedUrlExpiresIn ?? 3600\n );\n }\n }\n return null;\n }\n // ============================================================\n // Accessor Methods\n // ============================================================\n /**\n * Get the default storage adapter.\n *\n * @returns The default storage adapter (first registered plugin)\n * @throws Error if no storage plugin is configured\n */\n getDefaultAdapter() {\n return this.localAdapter;\n }\n /**\n * Get list of registered plugins.\n *\n * @returns Array of registered storage plugins\n */\n getPlugins() {\n return Array.from(this.plugins.values());\n }\n /**\n * Get the underlying storage adapter for a collection.\n *\n * Useful for passing to registerServices() which requires IStorageAdapter.\n *\n * @param collection - The collection slug (optional)\n * @returns The storage adapter instance\n */\n getAdapter(collection) {\n return this.getAdapterForCollection(collection);\n }\n /**\n * Check if a collection has a configured storage adapter.\n *\n * @param collection - The collection slug\n * @returns True if a plugin is configured for this collection\n */\n hasCollectionAdapter(collection) {\n return this.collectionAdapters.has(collection);\n }\n /**\n * Get list of collections with configured storage.\n *\n * @returns Array of collection slugs that have plugin storage\n */\n getConfiguredCollections() {\n return Array.from(this.collectionAdapters.keys());\n }\n /**\n * Check if any storage plugin is configured.\n *\n * @returns True if at least one storage plugin is registered\n */\n hasPlugins() {\n return this.plugins.size > 0;\n }\n};\nvar storageInstance = null;\nfunction initializeMediaStorage(config) {\n storageInstance = new MediaStorage(config);\n return storageInstance;\n}\nfunction getMediaStorage() {\n if (!storageInstance) {\n storageInstance = new MediaStorage();\n }\n return storageInstance;\n}\nfunction resetMediaStorage() {\n storageInstance = null;\n}\n\n// src/storage/sharp-loader.ts\nimport { createRequire } from \"module\";\nvar SHARP_INSTALL_COMMAND = \"npm install sharp\";\nvar requireFrom = createRequire(import.meta.url);\nvar injected = null;\nfunction imageFactoryOf(value) {\n return typeof value === \"function\" ? value : void 0;\n}\nasync function loadSharp(options) {\n if (injected) return injected;\n try {\n const loaded = await (options?.resolver ? options.resolver() : import(\"sharp\"));\n if (loaded === null || loaded === void 0) return null;\n return imageFactoryOf(loaded.default) ?? imageFactoryOf(loaded) ?? null;\n } catch {\n return null;\n }\n}\n\n// src/storage/image-processor.ts\nfunction refusesUpload(validity) {\n return validity === \"invalid\";\n}\nvar ImageProcessor = class {\n constructor(deps = {}) {\n this.deps = deps;\n }\n /** The library, or null when this install does not have it. */\n sharp() {\n return (this.deps.loader ?? loadSharp)();\n }\n /**\n * Whether this install can process images at all.\n *\n * Exposed so a caller can decide BEFORE doing work, and so the admin can say\n * what is missing once rather than letting each operation fail separately.\n */\n async canProcess() {\n return await this.sharp() !== null;\n }\n /**\n * Get image metadata without loading full image\n */\n async getMetadata(buffer) {\n const sharp = await this.sharp();\n if (!sharp) return null;\n const metadata = await sharp(buffer).metadata();\n return {\n width: metadata.width || 0,\n height: metadata.height || 0,\n format: metadata.format || \"unknown\",\n size: buffer.length\n };\n }\n /**\n * Generate thumbnail (300x300 by default, cropped to center)\n *\n * Uses \"cover\" fit to fill the entire 300x300 area while maintaining aspect ratio\n */\n async generateThumbnail(buffer, size = 300) {\n const sharp = await this.sharp();\n if (!sharp) return null;\n const processed = await sharp(buffer).resize(size, size, {\n fit: \"cover\",\n // Crop to fill entire area\n position: \"center\"\n // Crop from center\n }).jpeg({ quality: 80, progressive: true }).toBuffer({ resolveWithObject: true });\n return {\n buffer: processed.data,\n metadata: {\n width: processed.info.width,\n height: processed.info.height,\n format: processed.info.format,\n size: processed.data.length\n }\n };\n }\n /**\n * Optimize image (compress, convert to WebP if beneficial)\n *\n * Strategy:\n * - Small images (<100KB) and already WebP: return as-is\n * - Otherwise: convert to WebP with quality 80\n */\n async optimize(buffer, quality = 80) {\n const sharp = await this.sharp();\n if (!sharp) return null;\n const metadata = await sharp(buffer).metadata();\n if (buffer.length < 100 * 1024 && metadata.format === \"webp\") {\n return {\n buffer,\n metadata: {\n width: metadata.width || 0,\n height: metadata.height || 0,\n format: metadata.format,\n size: buffer.length\n }\n };\n }\n const processed = await sharp(buffer).webp({ quality, effort: 4 }).toBuffer({ resolveWithObject: true });\n return {\n buffer: processed.data,\n metadata: {\n width: processed.info.width,\n height: processed.info.height,\n format: \"webp\",\n size: processed.data.length\n }\n };\n }\n /**\n * Resize image to specific dimensions\n *\n * @param maxWidth Maximum width (maintains aspect ratio)\n * @param maxHeight Maximum height (maintains aspect ratio)\n */\n async resize(buffer, maxWidth, maxHeight) {\n const sharp = await this.sharp();\n if (!sharp) return null;\n const processed = await sharp(buffer).resize(maxWidth, maxHeight, {\n fit: \"inside\",\n // Fit within bounds, maintaining aspect ratio\n withoutEnlargement: true\n // Don't upscale small images\n }).toBuffer({ resolveWithObject: true });\n return {\n buffer: processed.data,\n metadata: {\n width: processed.info.width,\n height: processed.info.height,\n format: processed.info.format,\n size: processed.data.length\n }\n };\n }\n /**\n * Resize an image with focal point awareness and format conversion.\n *\n * When fit is 'cover' and a focal point is set, the crop anchors at that\n * point instead of center. Supports format conversion ('auto' outputs webp\n * for jpeg/png/tiff sources, keeps original for gif).\n */\n async resizeWithFocalPoint(buffer, options) {\n const sharp = await this.sharp();\n if (!sharp) return null;\n const quality = options.quality ?? 80;\n const metadata = await sharp(buffer).metadata();\n const originalFormat = metadata.format || \"jpeg\";\n let outputFormat = originalFormat;\n if (options.format && options.format !== \"auto\") {\n outputFormat = options.format;\n } else if (options.format === \"auto\") {\n const convertibleFormats = [\"jpeg\", \"png\", \"tiff\", \"jpg\"];\n if (convertibleFormats.includes(originalFormat)) {\n outputFormat = \"webp\";\n }\n }\n let pipeline = sharp(buffer);\n const srcW = metadata.width;\n const srcH = metadata.height;\n const tgtW = options.width;\n const tgtH = options.height;\n const hasFocalPoint = options.fit === \"cover\" && (options.focalX !== void 0 || options.focalY !== void 0) && tgtW !== void 0 && tgtH !== void 0 && srcW !== void 0 && srcH !== void 0 && tgtW > 0 && tgtH > 0 && srcW > 0 && srcH > 0;\n if (hasFocalPoint) {\n const fx = (options.focalX ?? 50) / 100;\n const fy = (options.focalY ?? 50) / 100;\n const tgtAspect = tgtW / tgtH;\n let cropW;\n let cropH;\n if (srcW / srcH > tgtAspect) {\n cropH = srcH;\n cropW = Math.round(srcH * tgtAspect);\n } else {\n cropW = srcW;\n cropH = Math.round(srcW / tgtAspect);\n }\n let left = Math.round(fx * srcW - cropW / 2);\n let top = Math.round(fy * srcH - cropH / 2);\n left = Math.max(0, Math.min(srcW - cropW, left));\n top = Math.max(0, Math.min(srcH - cropH, top));\n pipeline = pipeline.extract({ left, top, width: cropW, height: cropH }).resize(tgtW, tgtH, { fit: \"fill\" });\n } else {\n pipeline = pipeline.resize(\n options.width || void 0,\n options.height || void 0,\n {\n fit: options.fit,\n position: \"center\",\n withoutEnlargement: true\n }\n );\n }\n switch (outputFormat) {\n case \"webp\":\n pipeline = pipeline.webp({ quality, effort: 4 });\n break;\n case \"jpeg\":\n case \"jpg\":\n pipeline = pipeline.jpeg({ quality, progressive: true });\n outputFormat = \"jpeg\";\n break;\n case \"png\":\n pipeline = pipeline.png({ quality });\n break;\n case \"avif\":\n pipeline = pipeline.avif({ quality });\n break;\n default:\n break;\n }\n const result = await pipeline.toBuffer({ resolveWithObject: true });\n return {\n buffer: result.data,\n width: result.info.width,\n height: result.info.height,\n format: outputFormat,\n size: result.data.length\n };\n }\n /**\n * Check if buffer is a valid image\n */\n async isValidImage(buffer) {\n const sharp = await this.sharp();\n if (!sharp) return \"unknown\";\n try {\n await sharp(buffer).metadata();\n return \"valid\";\n } catch {\n return \"invalid\";\n }\n }\n /**\n * Get image dimensions quickly (without full processing)\n */\n async getDimensions(buffer) {\n try {\n const sharp = await this.sharp();\n if (!sharp) return null;\n const metadata = await sharp(buffer).metadata();\n if (metadata.width && metadata.height) {\n return { width: metadata.width, height: metadata.height };\n }\n return null;\n } catch {\n return null;\n }\n }\n};\nvar processorInstance = null;\nfunction getImageProcessor() {\n if (!processorInstance) {\n processorInstance = new ImageProcessor();\n }\n return processorInstance;\n}\nfunction resetImageProcessor() {\n processorInstance = null;\n}\n\n// src/storage/image-sizes.ts\nfunction getExtensionForFormat(format) {\n switch (format) {\n case \"jpeg\":\n case \"jpg\":\n return \"jpg\";\n case \"webp\":\n return \"webp\";\n case \"png\":\n return \"png\";\n case \"avif\":\n return \"avif\";\n default:\n return format;\n }\n}\nfunction getMimeTypeForFormat(format) {\n switch (format) {\n case \"jpeg\":\n case \"jpg\":\n return \"image/jpeg\";\n case \"webp\":\n return \"image/webp\";\n case \"png\":\n return \"image/png\";\n case \"avif\":\n return \"image/avif\";\n default:\n return `image/${format}`;\n }\n}\nfunction buildVariantFilename(originalFilename, sizeName, format) {\n const lastDot = originalFilename.lastIndexOf(\".\");\n const baseName = lastDot > 0 ? originalFilename.substring(0, lastDot) : originalFilename;\n const ext = getExtensionForFormat(format);\n return `${baseName}-${sizeName}.${ext}`;\n}\nasync function generateOneSize(processor, originalBuffer, originalFilename, sizeConfig, uploadFn, options) {\n if (!sizeConfig.width && !sizeConfig.height) return null;\n const resized = await processor.resizeWithFocalPoint(originalBuffer, {\n width: sizeConfig.width ?? void 0,\n height: sizeConfig.height ?? void 0,\n fit: sizeConfig.fit,\n quality: sizeConfig.quality,\n format: sizeConfig.format,\n focalX: options.focalX ?? void 0,\n focalY: options.focalY ?? void 0\n });\n if (!resized) return null;\n const variantFilename = buildVariantFilename(\n originalFilename,\n sizeConfig.name,\n resized.format\n );\n const mimeType = getMimeTypeForFormat(resized.format);\n const uploadResult = await uploadFn(resized.buffer, {\n filename: variantFilename,\n mimeType,\n folder: options.folder,\n collection: options.collection\n });\n return {\n url: uploadResult.url,\n path: uploadResult.path,\n width: resized.width,\n height: resized.height,\n filesize: resized.size,\n mimeType,\n filename: variantFilename\n };\n}\nasync function generateImageSizes(originalBuffer, originalFilename, sizes, uploadFn, options = {}) {\n if (sizes.length === 0) return {};\n const processor = getImageProcessor();\n const results = {};\n for (const sizeConfig of sizes) {\n try {\n const variant = await generateOneSize(\n processor,\n originalBuffer,\n originalFilename,\n sizeConfig,\n uploadFn,\n options\n );\n if (variant) results[sizeConfig.name] = variant;\n } catch (error) {\n console.warn(\n `[ImageSizes] Failed to generate size \"${sizeConfig.name}\":`,\n error instanceof Error ? error.message : error\n );\n }\n }\n return results;\n}\nasync function deleteImageSizes(sizes, deleteFn) {\n if (!sizes) return;\n const paths = Object.values(sizes).map((v) => v.path).filter(Boolean);\n await Promise.allSettled(paths.map((path) => deleteFn(path)));\n}\n\n// src/storage/retry.ts\nvar DEFAULT_OPTIONS = {\n maxAttempts: 3,\n baseDelayMs: 1e3,\n maxDelayMs: 3e4,\n backoffFactor: 2,\n jitter: true\n};\nfunction isTransientError(error) {\n if (!error) return false;\n if (error instanceof Error) {\n const message = error.message.toLowerCase();\n const name = error.name.toLowerCase();\n if (message.includes(\"timeout\") || message.includes(\"timed out\") || message.includes(\"etimedout\") || message.includes(\"econnreset\") || message.includes(\"econnrefused\") || message.includes(\"enotfound\") || message.includes(\"enetunreach\") || message.includes(\"socket hang up\") || message.includes(\"network\") || name.includes(\"timeout\") || name.includes(\"abort\")) {\n return true;\n }\n if (message.includes(\"rate limit\") || message.includes(\"too many\")) {\n return true;\n }\n }\n const errorAny = error;\n const metadata = errorAny.$metadata;\n if (errorAny.statusCode || errorAny.status || metadata?.httpStatusCode) {\n const status = errorAny.statusCode || errorAny.status || metadata?.httpStatusCode;\n const statusNum = typeof status === \"number\" ? status : Number(status);\n if (statusNum === 429 || statusNum >= 500 && statusNum < 600) {\n return true;\n }\n }\n if (errorAny.code) {\n const code = String(errorAny.code).toLowerCase();\n if (code.includes(\"timeout\") || code.includes(\"throttl\") || code.includes(\"serviceunavailable\") || code.includes(\"slowdown\") || code === \"econnreset\" || code === \"epipe\") {\n return true;\n }\n }\n return false;\n}\nfunction calculateDelay(attempt, options) {\n const { baseDelayMs, maxDelayMs, backoffFactor, jitter } = options;\n const exponentialDelay = baseDelayMs * Math.pow(backoffFactor, attempt - 1);\n const cappedDelay = Math.min(exponentialDelay, maxDelayMs);\n if (jitter) {\n const jitterAmount = cappedDelay * Math.random() * 0.5;\n return Math.floor(cappedDelay + jitterAmount);\n }\n return Math.floor(cappedDelay);\n}\nfunction sleep(ms) {\n return new Promise((resolve) => setTimeout(resolve, ms));\n}\nasync function withRetry(fn, options = {}) {\n const config = { ...DEFAULT_OPTIONS, ...options };\n const { maxAttempts, shouldRetry, onRetry } = {\n ...config,\n shouldRetry: options.shouldRetry ?? isTransientError,\n onRetry: options.onRetry\n };\n let lastError;\n for (let attempt = 1; attempt <= maxAttempts; attempt++) {\n try {\n return await fn();\n } catch (error) {\n lastError = error;\n const isLastAttempt = attempt >= maxAttempts;\n const canRetry = !isLastAttempt && shouldRetry(error, attempt);\n if (!canRetry) {\n throw error;\n }\n const delayMs = calculateDelay(attempt, config);\n if (onRetry) {\n onRetry(error, attempt, delayMs);\n }\n await sleep(delayMs);\n }\n }\n throw lastError;\n}\nfunction createRetryable(fn, options = {}) {\n return (...args) => withRetry(() => fn(...args), options);\n}\n\n// src/storage/svg-security.ts\nvar SVG_CSP_HEADER = \"script-src 'none'; style-src 'unsafe-inline'\";\nfunction isSvgMimeType(mimeType) {\n return mimeType.toLowerCase().trim() === \"image/svg+xml\";\n}\nfunction getSvgSecurityHeaders() {\n return {\n \"Content-Security-Policy\": SVG_CSP_HEADER,\n \"X-Content-Type-Options\": \"nosniff\"\n };\n}\n\n// src/storage/env-config.ts\nasync function ensureEnvLoaded() {\n if (typeof process !== \"undefined\" && !process.env._NEXTLY_ENV_LOADED) {\n try {\n const dotenv = await import(\"dotenv\");\n dotenv.config();\n process.env._NEXTLY_ENV_LOADED = \"true\";\n } catch {\n }\n }\n}\nasync function getStorageFromEnv() {\n await ensureEnvLoaded();\n const blobToken = process.env.BLOB_READ_WRITE_TOKEN;\n if (blobToken) {\n try {\n const pkg = \"@nextlyhq/storage-vercel-blob\";\n const { vercelBlobStorage } = await import(\n /* webpackIgnore: true */\n pkg\n );\n console.log(\n \"[Nextly] Storage: Vercel Blob (auto-detected from BLOB_READ_WRITE_TOKEN)\"\n );\n return [\n vercelBlobStorage({ token: blobToken, collections: { media: true } })\n ];\n } catch {\n console.warn(\n \"[Nextly] BLOB_READ_WRITE_TOKEN set but @nextlyhq/storage-vercel-blob not installed. Run: pnpm add @nextlyhq/storage-vercel-blob\"\n );\n }\n }\n const s3Bucket = process.env.S3_BUCKET;\n if (s3Bucket) {\n const region = process.env.S3_REGION;\n const accessKeyId = process.env.AWS_ACCESS_KEY_ID;\n const secretAccessKey = process.env.AWS_SECRET_ACCESS_KEY;\n if (!region || !accessKeyId || !secretAccessKey) {\n const missing = [];\n if (!region) missing.push(\"S3_REGION\");\n if (!accessKeyId) missing.push(\"AWS_ACCESS_KEY_ID\");\n if (!secretAccessKey) missing.push(\"AWS_SECRET_ACCESS_KEY\");\n console.warn(\n `[Nextly] S3_BUCKET set but missing: ${missing.join(\", \")}. Falling back to local storage.`\n );\n } else {\n try {\n const pkg = \"@nextlyhq/storage-s3\";\n const { s3Storage } = await import(\n /* webpackIgnore: true */\n pkg\n );\n console.log(\"[Nextly] Storage: S3 (auto-detected from S3_BUCKET)\");\n return [\n s3Storage({\n bucket: s3Bucket,\n region,\n accessKeyId,\n secretAccessKey,\n endpoint: process.env.S3_ENDPOINT,\n // eslint-disable-next-line turbo/no-undeclared-env-vars\n publicUrl: process.env.S3_PUBLIC_URL,\n // eslint-disable-next-line turbo/no-undeclared-env-vars\n forcePathStyle: process.env.S3_FORCE_PATH_STYLE === \"true\",\n collections: { media: true }\n })\n ];\n } catch {\n console.warn(\n \"[Nextly] S3_BUCKET set but @nextlyhq/storage-s3 not installed. Run: pnpm add @nextlyhq/storage-s3\"\n );\n }\n }\n }\n const uploadthingToken = process.env.UPLOADTHING_TOKEN;\n if (uploadthingToken) {\n try {\n const pkg = \"@nextlyhq/storage-uploadthing\";\n const { uploadthingStorage } = await import(\n /* webpackIgnore: true */\n pkg\n );\n console.log(\n \"[Nextly] Storage: Uploadthing (auto-detected from UPLOADTHING_TOKEN)\"\n );\n return [\n uploadthingStorage({\n token: uploadthingToken,\n collections: { media: true }\n })\n ];\n } catch {\n console.warn(\n \"[Nextly] UPLOADTHING_TOKEN set but @nextlyhq/storage-uploadthing not installed. Run: pnpm add @nextlyhq/storage-uploadthing\"\n );\n }\n }\n const { localStorage: localStorage2 } = await import(\"./local-plugin-X4OF5FWU.mjs\");\n console.log(\n \"[Nextly] Storage: Local disk (no cloud env vars detected, using ./public/uploads)\"\n );\n return [localStorage2({ collections: { media: true } })];\n}\n\nexport {\n SHARP_INSTALL_COMMAND,\n loadSharp,\n refusesUpload,\n ImageProcessor,\n getImageProcessor,\n resetImageProcessor,\n MediaStorage,\n initializeMediaStorage,\n getMediaStorage,\n resetMediaStorage,\n generateImageSizes,\n deleteImageSizes,\n isTransientError,\n withRetry,\n createRetryable,\n SVG_CSP_HEADER,\n isSvgMimeType,\n getSvgSecurityHeaders,\n getStorageFromEnv\n};\n","import {\n ImageProcessor,\n MediaStorage,\n SVG_CSP_HEADER,\n createRetryable,\n deleteImageSizes,\n generateImageSizes,\n getImageProcessor,\n getMediaStorage,\n getStorageFromEnv,\n getSvgSecurityHeaders,\n initializeMediaStorage,\n isSvgMimeType,\n isTransientError,\n resetImageProcessor,\n resetMediaStorage,\n withRetry\n} from \"../chunk-ANFXYTYN.mjs\";\nimport {\n BaseStorageAdapter,\n LocalStorageAdapter,\n localStorage\n} from \"../chunk-IJCL4XOT.mjs\";\nimport {\n fetchStoredBytes\n} from \"../chunk-C2VHLQKK.mjs\";\nimport {\n StorageReadTimeoutError,\n StorageReadTooLargeError,\n isStorageReadTimeout,\n isStorageReadTooLarge\n} from \"../chunk-W77AQUUQ.mjs\";\nimport \"../chunk-HJ5IQPOY.mjs\";\nimport \"../chunk-7P6ASYW6.mjs\";\nexport {\n BaseStorageAdapter,\n ImageProcessor,\n LocalStorageAdapter,\n MediaStorage,\n SVG_CSP_HEADER,\n StorageReadTimeoutError,\n StorageReadTooLargeError,\n createRetryable,\n deleteImageSizes,\n fetchStoredBytes,\n generateImageSizes,\n getImageProcessor,\n getMediaStorage,\n getStorageFromEnv,\n getSvgSecurityHeaders,\n initializeMediaStorage,\n isStorageReadTimeout,\n isStorageReadTooLarge,\n isSvgMimeType,\n isTransientError,\n localStorage,\n resetImageProcessor,\n resetMediaStorage,\n withRetry\n};\n","import {\n DEFAULT_READ_MAX_BYTES,\n DEFAULT_READ_TIMEOUT_MS,\n classifyFetchFailure,\n deadlineSignal,\n fetchStoredBytes,\n resolveReadBounds,\n withDeadline\n} from \"../chunk-C2VHLQKK.mjs\";\nimport \"../chunk-W77AQUUQ.mjs\";\nimport \"../chunk-HJ5IQPOY.mjs\";\nimport \"../chunk-7P6ASYW6.mjs\";\nexport {\n DEFAULT_READ_MAX_BYTES,\n DEFAULT_READ_TIMEOUT_MS,\n classifyFetchFailure,\n deadlineSignal,\n fetchStoredBytes,\n resolveReadBounds,\n withDeadline\n};\n","/**\n * Uploadthing Storage Adapter\n *\n * Implements the Nextly storage adapter interface using Uploadthing's UTApi\n * for server-side file operations. Files are served via Uploadthing's CDN.\n *\n * @example\n * ```typescript\n * const adapter = new UploadthingStorageAdapter({ token: process.env.UPLOADTHING_TOKEN });\n * const result = await adapter.upload(buffer, {\n * filename: 'photo.jpg',\n * mimeType: 'image/jpeg',\n * });\n * // result.url = 'https://utfs.io/f/abc123-photo.jpg'\n * ```\n */\n\nimport { BaseStorageAdapter } from \"nextly/storage\";\nimport type {\n UploadOptions,\n UploadResult,\n BulkDeleteResult,\n StorageReadOptions,\n} from \"nextly/storage\";\nimport {\n fetchStoredBytes,\n withDeadline,\n DEFAULT_READ_TIMEOUT_MS,\n} from \"nextly/storage/fetch-stored-bytes\";\nimport { UTApi } from \"uploadthing/server\";\n\n// ============================================================\n// Adapter Implementation\n// ============================================================\n\nexport class UploadthingStorageAdapter extends BaseStorageAdapter {\n private readonly utapi: UTApi;\n\n constructor(config: { token?: string }) {\n super();\n // UTApi reads UPLOADTHING_TOKEN from env if not provided\n this.utapi = new UTApi({\n ...(config.token ? { token: config.token } : {}),\n });\n }\n\n /**\n * Upload file to Uploadthing.\n * Creates a File object from the buffer and uploads via UTApi.\n */\n async upload(buffer: Buffer, options: UploadOptions): Promise<UploadResult> {\n const sanitized = this.sanitizeFilename(options.filename);\n\n // UTApi.uploadFiles expects File objects\n // Cast buffer to BlobPart to satisfy TS 5.9 strict ArrayBuffer typing\n const file = new File([buffer as unknown as BlobPart], sanitized, {\n type: options.mimeType,\n });\n\n // uploadFiles returns UploadFileResult[] — one result per file.\n // Default contentDisposition flipped from \"inline\" to \"attachment\".\n // An \"inline\" disposition lets the browser render the file\n // in-context (HTML, SVG, PDF with embedded JS) which can land as\n // XSS or drive-by; \"attachment\" forces the download dialog so the\n // user has to opt in to opening it. Adopters who genuinely want\n // inline rendering can still pass `contentDisposition: \"inline\"`\n // explicitly.\n const results = await this.utapi.uploadFiles([file], {\n contentDisposition: options.contentDisposition ?? \"attachment\",\n });\n\n // results is an array of { data: { key, url, ... } | null, error: ... | null }\n const result = results[0] as {\n data: { key: string; url: string } | null;\n error: { message: string } | null;\n };\n\n if (!result?.data) {\n const errorMsg = result?.error?.message ?? \"Unknown error\";\n throw new Error(`Uploadthing upload failed: ${errorMsg}`);\n }\n\n return {\n url: result.data.url,\n // Use the file key as the storage path (needed for deletion)\n path: result.data.key,\n };\n }\n\n /**\n * Delete file from Uploadthing by its file key.\n */\n async delete(filePath: string): Promise<void> {\n try {\n await this.utapi.deleteFiles([filePath], { keyType: \"fileKey\" });\n } catch {\n // Silently ignore deletion errors (file may already be gone)\n }\n }\n\n /**\n * Bulk delete files from Uploadthing.\n * UTApi natively supports batch deletion.\n */\n async bulkDelete(filePaths: string[]): Promise<BulkDeleteResult> {\n try {\n await this.utapi.deleteFiles(filePaths, { keyType: \"fileKey\" });\n return {\n successful: filePaths,\n failed: [],\n };\n } catch (error: unknown) {\n // If bulk delete fails entirely, report all as failed\n const message =\n error instanceof Error ? error.message : \"Bulk delete failed\";\n return {\n successful: [],\n failed: filePaths.map(fp => ({\n filePath: fp,\n error: message,\n })),\n };\n }\n }\n\n /**\n * Check if file exists on Uploadthing.\n * Uses getFileUrls - if it returns data with URLs, the file exists.\n */\n async exists(filePath: string): Promise<boolean> {\n try {\n const result = await this.utapi.getFileUrls([filePath], {\n keyType: \"fileKey\",\n });\n // getFileUrls returns { data: readonly [{ url, key }] }\n const items = Array.from(result.data);\n return items.length > 0 && !!items[0]?.url;\n } catch {\n return false;\n }\n }\n\n /**\n * Read a stored file back as bytes, or `null` when it is not there.\n *\n * A NETWORK round trip for the same reason as the Vercel adapter: the bytes\n * live on UploadThing's CDN. A caller serving these from its own origin has\n * to cache, or it pays the fetch on every request.\n *\n * The URL comes from `getFileUrls` rather than being assembled, because the\n * service owns the address and this adapter never chose it.\n *\n * @param filePath - File key\n * @returns The file's bytes, or `null` when no such key exists\n */\n async read(\n filePath: string,\n options?: StorageReadOptions\n ): Promise<Buffer | null> {\n /*\n * NOT wrapped in a catch. This is a batch lookup, so a key that is not\n * there comes back as an EMPTY `data` array — the branch below — rather\n * than as a rejection. What a rejection means instead is an invalid token,\n * an outage or a dropped connection, none of which say the file was\n * deleted; folding those into `null` would let a caller overwrite a file\n * that is still there.\n *\n * Stated as the reasoning rather than as a verified fact: the shape of a\n * missing-key response is not pinned by the SDK's types, so if this service\n * does reject for an absent key, `read` throws where the contract promises\n * `null`. That direction is the safe one to be wrong in — a caller sees an\n * error instead of a false \"deleted\" — which is why the uncertainty is\n * resolved toward propagating rather than toward swallowing.\n */\n /*\n * ONE deadline for both phases, started before the lookup — the key lookup\n * can stall as readily as the fetch, and it runs first.\n */\n const deadline = AbortSignal.timeout(\n options?.timeoutMs ?? DEFAULT_READ_TIMEOUT_MS\n );\n\n /*\n * RACED rather than cancelled, because this SDK cannot be cancelled.\n * UploadThing 7.7.4's `getFileUrls` reads only `keyType` and forwards no\n * signal, so passing one bounds nothing — an option accepted and ignored,\n * which reads as covered and behaves as absent. Racing at least returns\n * within the deadline this method advertised; the request itself runs on in\n * the background, which is the honest limit of what is available here.\n */\n const result = await withDeadline(\n this.utapi.getFileUrls([filePath], { keyType: \"fileKey\" }),\n deadline\n );\n const target = Array.from(result.data)[0]?.url;\n if (target === undefined) return null;\n\n /*\n * Outside the catch, as in the Vercel adapter, and through the same shared\n * helper: a fetch that fails after the key resolved is a transport failure,\n * and reporting it as absence would let a caller treat a live file as\n * deleted.\n */\n return await fetchStoredBytes(\n target,\n filePath,\n \"UploadThing\",\n options,\n deadline\n );\n }\n\n /**\n * Get public URL for a file.\n * Uploadthing files are served from utfs.io CDN.\n * The URL is stored at upload time, so this reconstructs it from the key.\n */\n getPublicUrl(filePath: string): string {\n // Uploadthing URLs follow the pattern: https://utfs.io/f/{fileKey}\n return `https://utfs.io/f/${filePath}`;\n }\n\n /**\n * Get storage type identifier.\n */\n getType(): string {\n return \"uploadthing\";\n }\n\n /**\n * Keep filename sanitization local so this adapter remains stable\n * even if upstream base adapter type declarations drift.\n */\n protected sanitizeFilename(filename: string): string {\n const basename = filename.split(/[/\\\\]/).pop() || filename;\n return basename.replace(/[^a-zA-Z0-9._-]/g, \"-\");\n }\n}\n","/**\n * Uploadthing Storage Plugin\n *\n * Factory function that creates a storage plugin for Uploadthing.\n * Returns a StoragePlugin that can be registered with MediaStorage.\n *\n * @example\n * ```typescript\n * import { uploadthingStorage } from '@nextlyhq/storage-uploadthing'\n * import { defineConfig } from 'nextly/config'\n *\n * export default defineConfig({\n * storage: [\n * uploadthingStorage({\n * token: process.env.UPLOADTHING_TOKEN,\n * collections: { media: true }\n * })\n * ]\n * })\n * ```\n */\n\nimport type { StoragePlugin } from \"nextly/storage\";\n\nimport { UploadthingStorageAdapter } from \"./adapter\";\nimport type { UploadthingStorageConfig } from \"./types\";\n\n/**\n * Create an Uploadthing storage plugin for Nextly.\n *\n * @param config - Uploadthing storage configuration\n * @returns A StoragePlugin that MediaStorage can register\n */\nexport function uploadthingStorage(\n config: UploadthingStorageConfig\n): StoragePlugin {\n // Handle disabled plugin\n if (config.enabled === false) {\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: {},\n adapter: null as unknown as StoragePlugin[\"adapter\"],\n };\n }\n\n // Token from config or env\n const token = config.token ?? process.env.UPLOADTHING_TOKEN;\n\n if (!token) {\n console.warn(\n \"[Nextly] Uploadthing token not provided. Set UPLOADTHING_TOKEN env var or pass token in config.\"\n );\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: {},\n adapter: null as unknown as StoragePlugin[\"adapter\"],\n };\n }\n\n const adapter = new UploadthingStorageAdapter({ token });\n\n return {\n name: \"uploadthing-storage\",\n type: \"uploadthing\",\n collections: config.collections,\n adapter,\n // Uploadthing supports client-side uploads via its own pattern\n // but we don't implement getClientUploadUrl here as it requires\n // Uploadthing's specific route handler setup\n };\n}\n"]}
package/dist/index.d.cts CHANGED
@@ -70,6 +70,26 @@ type StorageType = "s3" | "vercel-blob" | "local" | "uploadthing";
70
70
  * Information about a storage adapter's capabilities.
71
71
  * Returned by adapter.getInfo() method.
72
72
  */
73
+ /**
74
+ * Bounds for a read that pulls an object into memory.
75
+ *
76
+ * Reading a stored object means BUFFERING it, so its size is the caller's
77
+ * problem whether or not the caller says so. Left unbounded, a large object
78
+ * consumes as much process memory as it happens to occupy, and a stalled
79
+ * backend holds the request until something further out gives up.
80
+ *
81
+ * OPTIONAL, with the defaults living in the fetch layer rather than being
82
+ * restated per adapter — a second set of numbers agrees today and drifts. What
83
+ * this type exists for is the caller that already knows its own limit: the
84
+ * email attachment path caps a read at the configured attachment size, so an
85
+ * oversized object fails as a size error rather than after it has been read.
86
+ */
87
+ interface StorageReadOptions {
88
+ /** Refuse once this many bytes have arrived. Defaults to the fetch cap. */
89
+ maxBytes?: number;
90
+ /** Give up after this long. Defaults to the fetch deadline. */
91
+ timeoutMs?: number;
92
+ }
73
93
  interface StorageAdapterInfo {
74
94
  /** Storage type identifier */
75
95
  type: StorageType;
@@ -228,7 +248,7 @@ interface IStorageAdapter {
228
248
  /** Get storage type identifier */
229
249
  getType(): string;
230
250
  /** Read file contents from storage (optional - not all adapters support this) */
231
- read?(filePath: string): Promise<Buffer | null>;
251
+ read?(filePath: string, options?: StorageReadOptions): Promise<Buffer | null>;
232
252
  /** Get adapter info including capabilities (optional but recommended) */
233
253
  getInfo?(): StorageAdapterInfo;
234
254
  /** Get file metadata (optional - not all adapters support this) */
@@ -455,6 +475,20 @@ declare class UploadthingStorageAdapter extends BaseStorageAdapter {
455
475
  * Uses getFileUrls - if it returns data with URLs, the file exists.
456
476
  */
457
477
  exists(filePath: string): Promise<boolean>;
478
+ /**
479
+ * Read a stored file back as bytes, or `null` when it is not there.
480
+ *
481
+ * A NETWORK round trip for the same reason as the Vercel adapter: the bytes
482
+ * live on UploadThing's CDN. A caller serving these from its own origin has
483
+ * to cache, or it pays the fetch on every request.
484
+ *
485
+ * The URL comes from `getFileUrls` rather than being assembled, because the
486
+ * service owns the address and this adapter never chose it.
487
+ *
488
+ * @param filePath - File key
489
+ * @returns The file's bytes, or `null` when no such key exists
490
+ */
491
+ read(filePath: string, options?: StorageReadOptions): Promise<Buffer | null>;
458
492
  /**
459
493
  * Get public URL for a file.
460
494
  * Uploadthing files are served from utfs.io CDN.
package/dist/index.d.ts CHANGED
@@ -70,6 +70,26 @@ type StorageType = "s3" | "vercel-blob" | "local" | "uploadthing";
70
70
  * Information about a storage adapter's capabilities.
71
71
  * Returned by adapter.getInfo() method.
72
72
  */
73
+ /**
74
+ * Bounds for a read that pulls an object into memory.
75
+ *
76
+ * Reading a stored object means BUFFERING it, so its size is the caller's
77
+ * problem whether or not the caller says so. Left unbounded, a large object
78
+ * consumes as much process memory as it happens to occupy, and a stalled
79
+ * backend holds the request until something further out gives up.
80
+ *
81
+ * OPTIONAL, with the defaults living in the fetch layer rather than being
82
+ * restated per adapter — a second set of numbers agrees today and drifts. What
83
+ * this type exists for is the caller that already knows its own limit: the
84
+ * email attachment path caps a read at the configured attachment size, so an
85
+ * oversized object fails as a size error rather than after it has been read.
86
+ */
87
+ interface StorageReadOptions {
88
+ /** Refuse once this many bytes have arrived. Defaults to the fetch cap. */
89
+ maxBytes?: number;
90
+ /** Give up after this long. Defaults to the fetch deadline. */
91
+ timeoutMs?: number;
92
+ }
73
93
  interface StorageAdapterInfo {
74
94
  /** Storage type identifier */
75
95
  type: StorageType;
@@ -228,7 +248,7 @@ interface IStorageAdapter {
228
248
  /** Get storage type identifier */
229
249
  getType(): string;
230
250
  /** Read file contents from storage (optional - not all adapters support this) */
231
- read?(filePath: string): Promise<Buffer | null>;
251
+ read?(filePath: string, options?: StorageReadOptions): Promise<Buffer | null>;
232
252
  /** Get adapter info including capabilities (optional but recommended) */
233
253
  getInfo?(): StorageAdapterInfo;
234
254
  /** Get file metadata (optional - not all adapters support this) */
@@ -455,6 +475,20 @@ declare class UploadthingStorageAdapter extends BaseStorageAdapter {
455
475
  * Uses getFileUrls - if it returns data with URLs, the file exists.
456
476
  */
457
477
  exists(filePath: string): Promise<boolean>;
478
+ /**
479
+ * Read a stored file back as bytes, or `null` when it is not there.
480
+ *
481
+ * A NETWORK round trip for the same reason as the Vercel adapter: the bytes
482
+ * live on UploadThing's CDN. A caller serving these from its own origin has
483
+ * to cache, or it pays the fetch on every request.
484
+ *
485
+ * The URL comes from `getFileUrls` rather than being assembled, because the
486
+ * service owns the address and this adapter never chose it.
487
+ *
488
+ * @param filePath - File key
489
+ * @returns The file's bytes, or `null` when no such key exists
490
+ */
491
+ read(filePath: string, options?: StorageReadOptions): Promise<Buffer | null>;
458
492
  /**
459
493
  * Get public URL for a file.
460
494
  * Uploadthing files are served from utfs.io CDN.