@nextlyhq/storage-uploadthing 0.0.1

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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 NextlyHQ <info@nextlyhq.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ 'Software'), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
19
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
20
+ CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
21
+ TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
22
+ SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,75 @@
1
+ # @nextlyhq/storage-uploadthing
2
+
3
+ [UploadThing](https://uploadthing.com?utm_source=nextly&utm_medium=readme) storage adapter for Nextly. Stores media on UploadThing's CDN and serves them from `utfs.io`.
4
+
5
+ <p align="center">
6
+ <a href="https://www.npmjs.com/package/@nextlyhq/storage-uploadthing"><img alt="npm" src="https://img.shields.io/npm/v/@nextlyhq/storage-uploadthing?style=flat-square&label=npm&color=cb3837" /></a>
7
+ <a href="https://github.com/nextlyhq/nextly/blob/main/LICENSE.md"><img alt="License" src="https://img.shields.io/github/license/nextlyhq/nextly?style=flat-square&color=blue" /></a>
8
+ <a href="https://nextlyhq.com/docs"><img alt="Status" src="https://img.shields.io/badge/status-alpha-orange?style=flat-square" /></a>
9
+ </p>
10
+
11
+ > [!IMPORTANT]
12
+ > Nextly is in alpha. APIs may change before 1.0. Pin exact versions in production.
13
+
14
+ ## What it is
15
+
16
+ Stores Nextly media uploads on UploadThing. Useful if you already use UploadThing for the rest of your Next.js stack, or want a managed CDN with no AWS account setup.
17
+
18
+ > **You do not need this for development.** Nextly's default storage is local disk under `./public/uploads/`. Install this when you are ready to move uploads to UploadThing, typically for production.
19
+
20
+ ## Installation
21
+
22
+ ```bash
23
+ pnpm add @nextlyhq/storage-uploadthing
24
+ ```
25
+
26
+ ## Quick usage
27
+
28
+ Register the storage adapter in `nextly.config.ts`:
29
+
30
+ ```ts
31
+ import { defineConfig } from "nextly/config";
32
+ import { uploadthingStorage } from "@nextlyhq/storage-uploadthing";
33
+
34
+ export default defineConfig({
35
+ storage: [
36
+ uploadthingStorage({
37
+ token: process.env.UPLOADTHING_TOKEN!,
38
+ collections: { media: true },
39
+ }),
40
+ ],
41
+ });
42
+ ```
43
+
44
+ ## Required environment variables
45
+
46
+ | Variable | Required? | Default | Notes |
47
+ | ------------------- | --------- | ------- | ------------------------------------------------- |
48
+ | `UPLOADTHING_TOKEN` | yes | (none) | Find in the UploadThing dashboard under API keys. |
49
+
50
+ ## Main exports
51
+
52
+ - `uploadthingStorage`: plugin factory for `defineConfig.storage`
53
+ - `UploadthingStorageAdapter`: the adapter class (advanced)
54
+ - Type exports: `UploadthingStorageConfig`
55
+
56
+ ## Compatibility
57
+
58
+ | Tool | Version |
59
+ | ------------- | ------- |
60
+ | Node.js | 20+ |
61
+ | `uploadthing` | peer |
62
+ | `nextly` | 0.0.x |
63
+
64
+ ## Documentation
65
+
66
+ - [**Media and storage docs**](https://nextlyhq.com/docs/guides/media-storage)
67
+
68
+ ## Related packages
69
+
70
+ - [`@nextlyhq/storage-s3`](../storage-s3)
71
+ - [`@nextlyhq/storage-vercel-blob`](../storage-vercel-blob)
72
+
73
+ ## License
74
+
75
+ [MIT](../../LICENSE.md)
@@ -0,0 +1,14 @@
1
+ var __getOwnPropNames = Object.getOwnPropertyNames;
2
+ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, {
3
+ get: (a, b) => (typeof require !== "undefined" ? require : a)[b]
4
+ }) : x)(function(x) {
5
+ if (typeof require !== "undefined") return require.apply(this, arguments);
6
+ throw Error('Dynamic require of "' + x + '" is not supported');
7
+ });
8
+ var __commonJS = (cb, mod) => function __require2() {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ };
11
+
12
+ export { __commonJS, __require };
13
+ //# sourceMappingURL=chunk-3GDQP6AS.mjs.map
14
+ //# sourceMappingURL=chunk-3GDQP6AS.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"names":[],"mappings":"","file":"chunk-3GDQP6AS.mjs"}
@@ -0,0 +1,255 @@
1
+ import * as fs from 'fs/promises';
2
+ import * as path from 'path';
3
+
4
+ // ../nextly/dist/chunk-G2AA4QLC.mjs
5
+ var BaseStorageAdapter = class {
6
+ /**
7
+ * Get adapter info including capabilities.
8
+ *
9
+ * Default implementation that auto-detects capabilities by checking
10
+ * if getSignedUrl and getPresignedUploadUrl methods are implemented.
11
+ * Override in subclasses for more accurate capability reporting.
12
+ *
13
+ * @returns Adapter info with type, name, and capability flags
14
+ */
15
+ getInfo() {
16
+ const hasSignedUrls = "getSignedUrl" in this && typeof this.getSignedUrl === "function";
17
+ const hasClientUploads = "getPresignedUploadUrl" in this && typeof this.getPresignedUploadUrl === "function";
18
+ return {
19
+ type: this.getType(),
20
+ name: this.constructor.name,
21
+ supportsSignedUrls: hasSignedUrls,
22
+ supportsClientUploads: hasClientUploads
23
+ };
24
+ }
25
+ /**
26
+ * Sanitize filename to prevent directory traversal and storage issues.
27
+ *
28
+ * Security measures:
29
+ * - Remove path separators (/, \)
30
+ * - Keep only basename (no directories)
31
+ * - Replace problematic characters with hyphens
32
+ * - Preserve alphanumeric, dots, underscores, hyphens
33
+ *
34
+ * @param filename - Original filename to sanitize
35
+ * @returns Sanitized filename safe for storage
36
+ *
37
+ * @example
38
+ * ```typescript
39
+ * this.sanitizeFilename('../../../etc/passwd') // 'passwd'
40
+ * this.sanitizeFilename('my file (1).jpg') // 'my-file--1-.jpg'
41
+ * this.sanitizeFilename('photo.jpg') // 'photo.jpg'
42
+ * ```
43
+ */
44
+ sanitizeFilename(filename) {
45
+ const basename = filename.split(/[/\\]/).pop() || filename;
46
+ return basename.replace(/[^a-zA-Z0-9._-]/g, "-");
47
+ }
48
+ /**
49
+ * Generate a unique storage key with date-based prefix.
50
+ *
51
+ * Creates keys in format: {folder}/{year}/{month}/{uuid}-{sanitized-filename}
52
+ * This provides:
53
+ * - Unique keys via UUID to prevent collisions
54
+ * - Date-based organization for easier management
55
+ * - Readable filenames for debugging
56
+ *
57
+ * @param filename - Original filename (will be sanitized)
58
+ * @param folder - Optional folder/prefix for organizing uploads
59
+ * @returns Generated storage key
60
+ *
61
+ * @example
62
+ * ```typescript
63
+ * this.generateKey('photo.jpg')
64
+ * // 'uploads/2026/01/abc-123-...-photo.jpg'
65
+ *
66
+ * this.generateKey('doc.pdf', 'documents')
67
+ * // 'documents/2026/01/abc-123-...-doc.pdf'
68
+ * ```
69
+ */
70
+ generateKey(filename, folder) {
71
+ const sanitized = this.sanitizeFilename(filename);
72
+ const uuid = crypto.randomUUID();
73
+ const date = /* @__PURE__ */ new Date();
74
+ const year = date.getFullYear();
75
+ const month = String(date.getMonth() + 1).padStart(2, "0");
76
+ const prefix = folder ? `${folder}/${year}/${month}` : `uploads/${year}/${month}`;
77
+ return `${prefix}/${uuid}-${sanitized}`;
78
+ }
79
+ };
80
+ var gitignoreUpdated = false;
81
+ var LocalStorageAdapter = class extends BaseStorageAdapter {
82
+ basePath;
83
+ baseUrl;
84
+ constructor(config) {
85
+ super();
86
+ this.basePath = path.resolve(config.basePath);
87
+ this.baseUrl = config.baseUrl.replace(/\/+$/, "");
88
+ }
89
+ /**
90
+ * Upload file to local disk.
91
+ * Creates directories as needed and writes the file buffer.
92
+ */
93
+ async upload(buffer, options) {
94
+ const key = this.generateKey(options.filename, options.folder);
95
+ const fullPath = this.resolveAndValidate(key);
96
+ await fs.mkdir(path.dirname(fullPath), { recursive: true });
97
+ await fs.writeFile(fullPath, buffer);
98
+ await this.ensureGitignore();
99
+ return {
100
+ url: this.getPublicUrl(key),
101
+ path: key
102
+ };
103
+ }
104
+ /**
105
+ * Delete file from local disk.
106
+ * Silently succeeds if the file doesn't exist.
107
+ */
108
+ async delete(filePath) {
109
+ let fullPath;
110
+ try {
111
+ fullPath = this.resolveAndValidate(filePath);
112
+ } catch {
113
+ return;
114
+ }
115
+ try {
116
+ await fs.unlink(fullPath);
117
+ } catch (err) {
118
+ if (err.code !== "ENOENT") {
119
+ throw err;
120
+ }
121
+ }
122
+ }
123
+ /**
124
+ * Bulk delete files from local disk.
125
+ * Uses parallel unlinks with Promise.allSettled for best performance.
126
+ */
127
+ async bulkDelete(filePaths) {
128
+ const results = await Promise.allSettled(
129
+ filePaths.map(async (filePath) => {
130
+ await this.delete(filePath);
131
+ return filePath;
132
+ })
133
+ );
134
+ const successful = [];
135
+ const failed = [];
136
+ results.forEach((result, index) => {
137
+ if (result.status === "fulfilled") {
138
+ successful.push(filePaths[index]);
139
+ } else {
140
+ failed.push({
141
+ filePath: filePaths[index],
142
+ error: result.reason?.message || "Unknown error"
143
+ });
144
+ }
145
+ });
146
+ return { successful, failed };
147
+ }
148
+ /**
149
+ * Check if file exists on local disk.
150
+ */
151
+ async exists(filePath) {
152
+ try {
153
+ const fullPath = this.resolveAndValidate(filePath);
154
+ await fs.access(fullPath);
155
+ return true;
156
+ } catch {
157
+ return false;
158
+ }
159
+ }
160
+ /**
161
+ * Get public URL for a file.
162
+ * Returns baseUrl + relative path for Next.js static file serving.
163
+ */
164
+ getPublicUrl(filePath) {
165
+ const cleanPath = filePath.replace(/^\/+/, "");
166
+ return `${this.baseUrl}/${cleanPath}`;
167
+ }
168
+ /**
169
+ * Get storage type identifier.
170
+ */
171
+ getType() {
172
+ return "local";
173
+ }
174
+ /**
175
+ * Read file contents from local disk.
176
+ * Returns the file buffer, or null if file not found.
177
+ */
178
+ async read(filePath) {
179
+ try {
180
+ const fullPath = this.resolveAndValidate(filePath);
181
+ return await fs.readFile(fullPath);
182
+ } catch {
183
+ return null;
184
+ }
185
+ }
186
+ // ============================================================
187
+ // Private Helpers
188
+ // ============================================================
189
+ /**
190
+ * Resolve a relative file path to an absolute path within basePath.
191
+ * Throws if the resolved path would escape basePath (path traversal attack).
192
+ */
193
+ resolveAndValidate(filePath) {
194
+ const sanitized = filePath.replace(/^[/\\]+/, "").replace(/\.\.[/\\]/g, "");
195
+ const fullPath = path.resolve(this.basePath, sanitized);
196
+ if (!fullPath.startsWith(this.basePath)) {
197
+ throw new Error(
198
+ `Path traversal detected: ${filePath} resolves outside of storage directory`
199
+ );
200
+ }
201
+ return fullPath;
202
+ }
203
+ /**
204
+ * Auto-add the uploads directory to .gitignore on first upload.
205
+ * Prevents accidentally committing uploaded files to git.
206
+ */
207
+ async ensureGitignore() {
208
+ if (gitignoreUpdated) return;
209
+ gitignoreUpdated = true;
210
+ try {
211
+ const projectRoot = path.resolve(this.basePath, "..", "..");
212
+ const gitignorePath = path.join(projectRoot, ".gitignore");
213
+ let content = "";
214
+ try {
215
+ content = await fs.readFile(gitignorePath, "utf-8");
216
+ } catch {
217
+ }
218
+ const uploadsDirRelative = path.relative(projectRoot, this.basePath);
219
+ const ignorePattern = uploadsDirRelative + "/";
220
+ if (!content.includes(ignorePattern)) {
221
+ const newEntry = `
222
+ # Nextly local uploads (auto-added)
223
+ ${ignorePattern}
224
+ `;
225
+ await fs.writeFile(gitignorePath, content + newEntry, "utf-8");
226
+ }
227
+ } catch {
228
+ }
229
+ }
230
+ };
231
+ function localStorage(config) {
232
+ if (config.enabled === false) {
233
+ return {
234
+ name: "local-storage",
235
+ type: "local",
236
+ collections: {},
237
+ adapter: null
238
+ };
239
+ }
240
+ const adapter = new LocalStorageAdapter({
241
+ basePath: config.basePath ?? "./public/uploads",
242
+ baseUrl: config.baseUrl ?? "/uploads"
243
+ });
244
+ return {
245
+ name: "local-storage",
246
+ type: "local",
247
+ collections: config.collections,
248
+ adapter
249
+ // Local storage doesn't support presigned URLs or signed downloads
250
+ };
251
+ }
252
+
253
+ export { BaseStorageAdapter, localStorage };
254
+ //# sourceMappingURL=chunk-CV4XIHXE.mjs.map
255
+ //# sourceMappingURL=chunk-CV4XIHXE.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../../nextly/dist/chunk-G2AA4QLC.mjs"],"names":[],"mappings":"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","file":"chunk-CV4XIHXE.mjs","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 * // 'uploads/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}` : `uploads/${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"]}
package/dist/index.cjs ADDED
@@ -0,0 +1,242 @@
1
+ 'use strict';
2
+
3
+ var server = require('uploadthing/server');
4
+
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __esm = (fn, res) => function __init() {
7
+ return fn && (res = (0, fn[__getOwnPropNames(fn)[0]])(fn = 0)), res;
8
+ };
9
+
10
+ // ../nextly/dist/chunk-G2AA4QLC.mjs
11
+ var BaseStorageAdapter;
12
+ var init_chunk_G2AA4QLC = __esm({
13
+ "../nextly/dist/chunk-G2AA4QLC.mjs"() {
14
+ BaseStorageAdapter = class {
15
+ /**
16
+ * Get adapter info including capabilities.
17
+ *
18
+ * Default implementation that auto-detects capabilities by checking
19
+ * if getSignedUrl and getPresignedUploadUrl methods are implemented.
20
+ * Override in subclasses for more accurate capability reporting.
21
+ *
22
+ * @returns Adapter info with type, name, and capability flags
23
+ */
24
+ getInfo() {
25
+ const hasSignedUrls = "getSignedUrl" in this && typeof this.getSignedUrl === "function";
26
+ const hasClientUploads = "getPresignedUploadUrl" in this && typeof this.getPresignedUploadUrl === "function";
27
+ return {
28
+ type: this.getType(),
29
+ name: this.constructor.name,
30
+ supportsSignedUrls: hasSignedUrls,
31
+ supportsClientUploads: hasClientUploads
32
+ };
33
+ }
34
+ /**
35
+ * Sanitize filename to prevent directory traversal and storage issues.
36
+ *
37
+ * Security measures:
38
+ * - Remove path separators (/, \)
39
+ * - Keep only basename (no directories)
40
+ * - Replace problematic characters with hyphens
41
+ * - Preserve alphanumeric, dots, underscores, hyphens
42
+ *
43
+ * @param filename - Original filename to sanitize
44
+ * @returns Sanitized filename safe for storage
45
+ *
46
+ * @example
47
+ * ```typescript
48
+ * this.sanitizeFilename('../../../etc/passwd') // 'passwd'
49
+ * this.sanitizeFilename('my file (1).jpg') // 'my-file--1-.jpg'
50
+ * this.sanitizeFilename('photo.jpg') // 'photo.jpg'
51
+ * ```
52
+ */
53
+ sanitizeFilename(filename) {
54
+ const basename = filename.split(/[/\\]/).pop() || filename;
55
+ return basename.replace(/[^a-zA-Z0-9._-]/g, "-");
56
+ }
57
+ /**
58
+ * Generate a unique storage key with date-based prefix.
59
+ *
60
+ * Creates keys in format: {folder}/{year}/{month}/{uuid}-{sanitized-filename}
61
+ * This provides:
62
+ * - Unique keys via UUID to prevent collisions
63
+ * - Date-based organization for easier management
64
+ * - Readable filenames for debugging
65
+ *
66
+ * @param filename - Original filename (will be sanitized)
67
+ * @param folder - Optional folder/prefix for organizing uploads
68
+ * @returns Generated storage key
69
+ *
70
+ * @example
71
+ * ```typescript
72
+ * this.generateKey('photo.jpg')
73
+ * // 'uploads/2026/01/abc-123-...-photo.jpg'
74
+ *
75
+ * this.generateKey('doc.pdf', 'documents')
76
+ * // 'documents/2026/01/abc-123-...-doc.pdf'
77
+ * ```
78
+ */
79
+ generateKey(filename, folder) {
80
+ const sanitized = this.sanitizeFilename(filename);
81
+ const uuid = crypto.randomUUID();
82
+ const date = /* @__PURE__ */ new Date();
83
+ const year = date.getFullYear();
84
+ const month = String(date.getMonth() + 1).padStart(2, "0");
85
+ const prefix = folder ? `${folder}/${year}/${month}` : `uploads/${year}/${month}`;
86
+ return `${prefix}/${uuid}-${sanitized}`;
87
+ }
88
+ };
89
+ }
90
+ });
91
+
92
+ // ../nextly/dist/chunk-7P6ASYW6.mjs
93
+ var init_chunk_7P6ASYW6 = __esm({
94
+ "../nextly/dist/chunk-7P6ASYW6.mjs"() {
95
+ }
96
+ });
97
+
98
+ // ../nextly/dist/chunk-EGXBZCGC.mjs
99
+ init_chunk_G2AA4QLC();
100
+
101
+ // ../nextly/dist/storage/index.mjs
102
+ init_chunk_G2AA4QLC();
103
+ init_chunk_7P6ASYW6();
104
+ var UploadthingStorageAdapter = class extends BaseStorageAdapter {
105
+ utapi;
106
+ constructor(config) {
107
+ super();
108
+ this.utapi = new server.UTApi({
109
+ ...config.token ? { token: config.token } : {}
110
+ });
111
+ }
112
+ /**
113
+ * Upload file to Uploadthing.
114
+ * Creates a File object from the buffer and uploads via UTApi.
115
+ */
116
+ async upload(buffer, options) {
117
+ const sanitized = this.sanitizeFilename(options.filename);
118
+ const file = new File([buffer], sanitized, {
119
+ type: options.mimeType
120
+ });
121
+ const results = await this.utapi.uploadFiles([file], {
122
+ contentDisposition: options.contentDisposition ?? "attachment"
123
+ });
124
+ const result = results[0];
125
+ if (!result?.data) {
126
+ const errorMsg = result?.error?.message ?? "Unknown error";
127
+ throw new Error(`Uploadthing upload failed: ${errorMsg}`);
128
+ }
129
+ return {
130
+ url: result.data.url,
131
+ // Use the file key as the storage path (needed for deletion)
132
+ path: result.data.key
133
+ };
134
+ }
135
+ /**
136
+ * Delete file from Uploadthing by its file key.
137
+ */
138
+ async delete(filePath) {
139
+ try {
140
+ await this.utapi.deleteFiles([filePath], { keyType: "fileKey" });
141
+ } catch {
142
+ }
143
+ }
144
+ /**
145
+ * Bulk delete files from Uploadthing.
146
+ * UTApi natively supports batch deletion.
147
+ */
148
+ async bulkDelete(filePaths) {
149
+ try {
150
+ await this.utapi.deleteFiles(filePaths, { keyType: "fileKey" });
151
+ return {
152
+ successful: filePaths,
153
+ failed: []
154
+ };
155
+ } catch (error) {
156
+ const message = error instanceof Error ? error.message : "Bulk delete failed";
157
+ return {
158
+ successful: [],
159
+ failed: filePaths.map((fp) => ({
160
+ filePath: fp,
161
+ error: message
162
+ }))
163
+ };
164
+ }
165
+ }
166
+ /**
167
+ * Check if file exists on Uploadthing.
168
+ * Uses getFileUrls - if it returns data with URLs, the file exists.
169
+ */
170
+ async exists(filePath) {
171
+ try {
172
+ const result = await this.utapi.getFileUrls([filePath], {
173
+ keyType: "fileKey"
174
+ });
175
+ const items = Array.from(result.data);
176
+ return items.length > 0 && !!items[0]?.url;
177
+ } catch {
178
+ return false;
179
+ }
180
+ }
181
+ /**
182
+ * Get public URL for a file.
183
+ * Uploadthing files are served from utfs.io CDN.
184
+ * The URL is stored at upload time, so this reconstructs it from the key.
185
+ */
186
+ getPublicUrl(filePath) {
187
+ return `https://utfs.io/f/${filePath}`;
188
+ }
189
+ /**
190
+ * Get storage type identifier.
191
+ */
192
+ getType() {
193
+ return "uploadthing";
194
+ }
195
+ /**
196
+ * Keep filename sanitization local so this adapter remains stable
197
+ * even if upstream base adapter type declarations drift.
198
+ */
199
+ sanitizeFilename(filename) {
200
+ const basename = filename.split(/[/\\]/).pop() || filename;
201
+ return basename.replace(/[^a-zA-Z0-9._-]/g, "-");
202
+ }
203
+ };
204
+
205
+ // src/plugin.ts
206
+ function uploadthingStorage(config) {
207
+ if (config.enabled === false) {
208
+ return {
209
+ name: "uploadthing-storage",
210
+ type: "uploadthing",
211
+ collections: {},
212
+ adapter: null
213
+ };
214
+ }
215
+ const token = config.token ?? process.env.UPLOADTHING_TOKEN;
216
+ if (!token) {
217
+ console.warn(
218
+ "[Nextly] Uploadthing token not provided. Set UPLOADTHING_TOKEN env var or pass token in config."
219
+ );
220
+ return {
221
+ name: "uploadthing-storage",
222
+ type: "uploadthing",
223
+ collections: {},
224
+ adapter: null
225
+ };
226
+ }
227
+ const adapter = new UploadthingStorageAdapter({ token });
228
+ return {
229
+ name: "uploadthing-storage",
230
+ type: "uploadthing",
231
+ collections: config.collections,
232
+ adapter
233
+ // Uploadthing supports client-side uploads via its own pattern
234
+ // but we don't implement getClientUploadUrl here as it requires
235
+ // Uploadthing's specific route handler setup
236
+ };
237
+ }
238
+
239
+ exports.UploadthingStorageAdapter = UploadthingStorageAdapter;
240
+ exports.uploadthingStorage = uploadthingStorage;
241
+ //# sourceMappingURL=index.cjs.map
242
+ //# sourceMappingURL=index.cjs.map