img-upload-kit 1.1.1 → 2.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.
Files changed (111) hide show
  1. package/CHANGELOG.md +89 -74
  2. package/MIGRATION.md +192 -0
  3. package/README.md +243 -259
  4. package/dist/bin.js +1186 -0
  5. package/dist/bin.js.map +1 -0
  6. package/dist/blur.cjs +40 -412
  7. package/dist/blur.cjs.map +1 -1
  8. package/dist/blur.d.cts +5 -5
  9. package/dist/blur.d.ts +5 -5
  10. package/dist/blur.js +39 -393
  11. package/dist/blur.js.map +1 -1
  12. package/dist/blurDataUtils-DveBRtqE.d.cts +43 -0
  13. package/dist/blurDataUtils-yx9Q6HlU.d.ts +43 -0
  14. package/dist/blurService-D5TpMNji.d.ts +83 -0
  15. package/dist/blurService-DacwylJx.d.cts +83 -0
  16. package/dist/chunk-25GYKQ6J.js +158 -0
  17. package/dist/chunk-25GYKQ6J.js.map +1 -0
  18. package/dist/chunk-34L4TEE3.cjs +226 -0
  19. package/dist/chunk-34L4TEE3.cjs.map +1 -0
  20. package/dist/chunk-DFCSN3X7.cjs +158 -0
  21. package/dist/chunk-DFCSN3X7.cjs.map +1 -0
  22. package/dist/chunk-EWVIL57J.js +294 -0
  23. package/dist/chunk-EWVIL57J.js.map +1 -0
  24. package/dist/chunk-FP5YIWG5.cjs +135 -0
  25. package/dist/chunk-FP5YIWG5.cjs.map +1 -0
  26. package/dist/chunk-HILELQGS.js +226 -0
  27. package/dist/chunk-HILELQGS.js.map +1 -0
  28. package/dist/chunk-IDLLCVGB.js +2370 -0
  29. package/dist/chunk-IDLLCVGB.js.map +1 -0
  30. package/dist/chunk-J6A44Y3W.js +514 -0
  31. package/dist/chunk-J6A44Y3W.js.map +1 -0
  32. package/dist/chunk-LOSVFO56.cjs +145 -0
  33. package/dist/chunk-LOSVFO56.cjs.map +1 -0
  34. package/dist/chunk-MBQJ6NVJ.cjs +294 -0
  35. package/dist/chunk-MBQJ6NVJ.cjs.map +1 -0
  36. package/dist/chunk-MHGQEBPW.js +145 -0
  37. package/dist/chunk-MHGQEBPW.js.map +1 -0
  38. package/dist/chunk-QHU7FUO4.cjs +37 -0
  39. package/dist/chunk-QHU7FUO4.cjs.map +1 -0
  40. package/dist/chunk-S2IFAWJK.js +37 -0
  41. package/dist/chunk-S2IFAWJK.js.map +1 -0
  42. package/dist/chunk-TA5Y64JM.cjs +2370 -0
  43. package/dist/chunk-TA5Y64JM.cjs.map +1 -0
  44. package/dist/chunk-VYVZVUNI.js +135 -0
  45. package/dist/chunk-VYVZVUNI.js.map +1 -0
  46. package/dist/chunk-XKQMCY5Z.cjs +514 -0
  47. package/dist/chunk-XKQMCY5Z.cjs.map +1 -0
  48. package/dist/cli.cjs +273 -76
  49. package/dist/cli.cjs.map +1 -1
  50. package/dist/cli.js +269 -73
  51. package/dist/cli.js.map +1 -1
  52. package/dist/headless.cjs +8 -1880
  53. package/dist/headless.cjs.map +1 -1
  54. package/dist/headless.d.cts +3 -1
  55. package/dist/headless.d.ts +3 -1
  56. package/dist/headless.js +25 -1886
  57. package/dist/headless.js.map +1 -1
  58. package/dist/index-BIdnqtW8.d.ts +234 -0
  59. package/dist/index-Brk_uHs6.d.cts +234 -0
  60. package/dist/{index-FVXXKf2v.d.ts → index-CPMlaQsC.d.ts} +76 -153
  61. package/dist/{index-FVXXKf2v.d.cts → index-DHINe-rA.d.cts} +76 -153
  62. package/dist/index.cjs +64 -532
  63. package/dist/index.cjs.map +1 -1
  64. package/dist/index.d.cts +7 -4
  65. package/dist/index.d.ts +7 -4
  66. package/dist/index.js +63 -504
  67. package/dist/index.js.map +1 -1
  68. package/dist/{memoryAdapter-CBu7_3Pp.d.ts → memoryAdapter-Ck0ccZbF.d.cts} +3 -2
  69. package/dist/{memoryAdapter-BJjyLR-1.d.cts → memoryAdapter-DxEKaG0b.d.ts} +3 -2
  70. package/dist/placeholders-C4ftwaII.d.ts +5 -0
  71. package/dist/placeholders-x_3rt65M.d.cts +5 -0
  72. package/dist/react.cjs +1952 -3257
  73. package/dist/react.cjs.map +1 -1
  74. package/dist/react.d.cts +188 -50
  75. package/dist/react.d.ts +188 -50
  76. package/dist/react.js +1763 -3013
  77. package/dist/react.js.map +1 -1
  78. package/dist/server-handler.cjs +14 -0
  79. package/dist/server-handler.cjs.map +1 -0
  80. package/dist/server-handler.d.cts +3 -0
  81. package/dist/server-handler.d.ts +3 -0
  82. package/dist/server-handler.js +14 -0
  83. package/dist/server-handler.js.map +1 -0
  84. package/dist/server.cjs +1096 -532
  85. package/dist/server.cjs.map +1 -1
  86. package/dist/server.d.cts +77 -154
  87. package/dist/server.d.ts +77 -154
  88. package/dist/server.js +1050 -448
  89. package/dist/server.js.map +1 -1
  90. package/dist/storage.cjs +239 -232
  91. package/dist/storage.cjs.map +1 -1
  92. package/dist/storage.d.cts +21 -5
  93. package/dist/storage.d.ts +21 -5
  94. package/dist/storage.js +227 -207
  95. package/dist/storage.js.map +1 -1
  96. package/dist/styles/animated-image.css +112 -0
  97. package/dist/styles/dropzone-and-tiles.css +247 -0
  98. package/dist/styles/modals-and-dock.css +248 -0
  99. package/dist/styles/primitives.css +197 -0
  100. package/dist/styles/quality-and-inputs.css +184 -0
  101. package/dist/styles/tokens.css +123 -0
  102. package/dist/styles.css +235 -220
  103. package/dist/types-BrP-o9nd.d.cts +85 -0
  104. package/dist/types-sZ3KoFQY.d.ts +85 -0
  105. package/dist/types-yShGNQnW.d.cts +61 -0
  106. package/dist/types-yShGNQnW.d.ts +61 -0
  107. package/dist/{types-CiLY0DwB.d.cts → upload-C5Trd6Dh.d.cts} +27 -53
  108. package/dist/{types-CiLY0DwB.d.ts → upload-C5Trd6Dh.d.ts} +27 -53
  109. package/package.json +92 -32
  110. package/dist/blurService-DqsnfPTd.d.cts +0 -172
  111. package/dist/blurService-DqsnfPTd.d.ts +0 -172
package/dist/storage.js CHANGED
@@ -1,14 +1,41 @@
1
- import fs from 'fs/promises';
2
- import path from 'path';
1
+ import {
2
+ createMemoryStorageAdapter,
3
+ createStorageAdapter,
4
+ toPaginatedListResult,
5
+ validateStorageMetadata
6
+ } from "./chunk-MHGQEBPW.js";
7
+ import {
8
+ DEFAULT_PUBLIC_STORAGE_PREFIX,
9
+ parsePublicStorageUrl
10
+ } from "./chunk-S2IFAWJK.js";
3
11
 
4
12
  // src/storage/localFsAdapter.ts
13
+ import fs from "fs/promises";
14
+ import path from "path";
5
15
  function sanitizeRelativePath(inputPath) {
16
+ if (!inputPath || typeof inputPath !== "string") {
17
+ throw new Error("Invalid storage path");
18
+ }
19
+ if (inputPath.includes(":") || inputPath.includes("\0")) {
20
+ throw new Error("Path traversal blocked in LocalFsStorageAdapter");
21
+ }
6
22
  const normalized = inputPath.replace(/\\/g, "/").replace(/^\/+/, "");
7
- const parts = normalized.split("/").filter((segment) => segment && segment !== "." && segment !== "..");
8
- if (parts.length === 0) {
23
+ const rawSegments = normalized.split("/").filter(Boolean);
24
+ if (rawSegments.length === 0) {
9
25
  throw new Error("Invalid storage path");
10
26
  }
11
- return parts.join("/");
27
+ for (const segment of rawSegments) {
28
+ if (segment === "." || segment === "..") {
29
+ throw new Error("Invalid storage path: path traversal blocked");
30
+ }
31
+ }
32
+ return rawSegments.join("/");
33
+ }
34
+ function assertContainedInRoot(resolvedRoot, targetPath) {
35
+ const relative = path.relative(resolvedRoot, targetPath);
36
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
37
+ throw new Error("Path traversal blocked in LocalFsStorageAdapter");
38
+ }
12
39
  }
13
40
  function createLocalFsStorageAdapter(options) {
14
41
  const resolvedRoot = path.resolve(options.rootDir);
@@ -19,35 +46,78 @@ function createLocalFsStorageAdapter(options) {
19
46
  const cleanRel = sanitizeRelativePath(relativePath);
20
47
  const subPath = includeBucket ? `${cleanBucket}/${cleanRel}` : cleanRel;
21
48
  const diskPath = path.resolve(resolvedRoot, subPath);
22
- if (!diskPath.startsWith(resolvedRoot)) {
23
- throw new Error("Path traversal blocked in LocalFsStorageAdapter");
24
- }
49
+ assertContainedInRoot(resolvedRoot, diskPath);
25
50
  return { diskPath, subPath, cleanRel };
26
51
  };
27
52
  const getPublicUrl = (bucket, relativePath) => {
28
53
  const { subPath } = resolveDiskTarget(bucket, relativePath);
29
54
  return `${baseUrl}/${subPath}`;
30
55
  };
56
+ const parseUrl = (url) => {
57
+ if (!url || typeof url !== "string") return null;
58
+ try {
59
+ const cleanUrl = url.split(/[?#]/)[0];
60
+ const prefix = `${baseUrl}/`;
61
+ let subPath = null;
62
+ if (cleanUrl.startsWith(prefix)) {
63
+ subPath = cleanUrl.slice(prefix.length);
64
+ } else if (baseUrl.startsWith("/")) {
65
+ const parsed = new URL(cleanUrl, "http://localhost");
66
+ const isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
67
+ const isRelative = !cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://");
68
+ if ((isLocal || isRelative) && parsed.pathname.startsWith(prefix)) {
69
+ subPath = parsed.pathname.slice(prefix.length);
70
+ }
71
+ }
72
+ if (!subPath) return null;
73
+ const decoded = decodeURIComponent(subPath).replace(/\\/g, "/").replace(/^\/+/, "");
74
+ if (!decoded || decoded.includes(":") || decoded.includes("\0")) {
75
+ return null;
76
+ }
77
+ const segments = decoded.split("/").filter(Boolean);
78
+ if (segments.length === 0 || segments.includes("..") || segments.includes(".")) {
79
+ return null;
80
+ }
81
+ const diskPath = path.resolve(resolvedRoot, ...segments);
82
+ const relative = path.relative(resolvedRoot, diskPath);
83
+ if (relative.startsWith("..") || path.isAbsolute(relative)) {
84
+ return null;
85
+ }
86
+ if (includeBucket) {
87
+ if (segments.length < 2) return null;
88
+ return {
89
+ bucket: segments[0],
90
+ path: segments.slice(1).join("/")
91
+ };
92
+ }
93
+ return { path: segments.join("/") };
94
+ } catch {
95
+ return null;
96
+ }
97
+ };
31
98
  return {
32
99
  async upload(params) {
33
100
  const { diskPath, cleanRel } = resolveDiskTarget(
34
101
  params.bucket,
35
102
  params.path
36
103
  );
37
- if (params.upsert === false) {
38
- try {
39
- await fs.access(diskPath);
40
- throw new Error(
104
+ await fs.mkdir(path.dirname(diskPath), { recursive: true });
105
+ try {
106
+ await fs.writeFile(
107
+ diskPath,
108
+ params.data,
109
+ params.upsert === false ? { flag: "wx" } : void 0
110
+ );
111
+ } catch (err) {
112
+ if (err.code === "EEXIST") {
113
+ const existsError = new Error(
41
114
  `File already exists at ${params.bucket}/${cleanRel}`
42
115
  );
43
- } catch (err) {
44
- if (err instanceof Error && err.message.includes("File already exists")) {
45
- throw err;
46
- }
116
+ existsError.code = "EEXIST";
117
+ throw existsError;
47
118
  }
119
+ throw err;
48
120
  }
49
- await fs.mkdir(path.dirname(diskPath), { recursive: true });
50
- await fs.writeFile(diskPath, params.data);
51
121
  return {
52
122
  bucket: params.bucket,
53
123
  path: cleanRel,
@@ -64,42 +134,31 @@ function createLocalFsStorageAdapter(options) {
64
134
  }
65
135
  }
66
136
  },
137
+ async exists(bucket, relativePath) {
138
+ try {
139
+ const { diskPath } = resolveDiskTarget(bucket, relativePath);
140
+ const stat = await fs.stat(diskPath);
141
+ return stat.isFile();
142
+ } catch {
143
+ return false;
144
+ }
145
+ },
67
146
  getPublicUrl,
147
+ parseUrl,
68
148
  async deleteByUrl(url) {
69
- if (!url || typeof url !== "string") return false;
149
+ const parsed = parseUrl(url);
150
+ if (!parsed) return false;
70
151
  try {
71
- const cleanUrl = url.split(/[?#]/)[0];
72
- const prefix = `${baseUrl}/`;
73
- let subPath = null;
74
- if (cleanUrl.startsWith(prefix)) {
75
- subPath = cleanUrl.slice(prefix.length);
76
- } else if (baseUrl.startsWith("/")) {
77
- const parsed = new URL(cleanUrl, "http://localhost");
78
- const isLocal = parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
79
- const isRelative = !cleanUrl.startsWith("http://") && !cleanUrl.startsWith("https://");
80
- if ((isLocal || isRelative) && parsed.pathname.startsWith(prefix)) {
81
- subPath = parsed.pathname.slice(prefix.length);
82
- }
83
- }
84
- if (!subPath) return false;
85
- const decoded = decodeURIComponent(subPath).replace(/\\/g, "/").replace(/^\/+/, "");
86
- if (!decoded) return false;
87
- const segments = decoded.split("/").filter(Boolean);
88
- if (segments.length === 0 || segments.includes("..") || segments.includes(".")) {
89
- return false;
90
- }
91
- const diskPath = path.resolve(resolvedRoot, ...segments);
92
- const relative = path.relative(resolvedRoot, diskPath);
93
- if (relative.startsWith("..") || path.isAbsolute(relative)) {
94
- return false;
95
- }
152
+ const subSegments = includeBucket && parsed.bucket ? [parsed.bucket, ...parsed.path.split("/")] : parsed.path.split("/");
153
+ const diskPath = path.resolve(resolvedRoot, ...subSegments);
154
+ assertContainedInRoot(resolvedRoot, diskPath);
96
155
  await fs.unlink(diskPath);
97
156
  return true;
98
157
  } catch {
99
158
  return false;
100
159
  }
101
160
  },
102
- async list(bucket, folder) {
161
+ async list(bucket, folder, listOptions) {
103
162
  const cleanBucket = sanitizeRelativePath(bucket);
104
163
  const cleanFolder = folder ? sanitizeRelativePath(folder) : "";
105
164
  const targetDir = path.resolve(
@@ -107,9 +166,7 @@ function createLocalFsStorageAdapter(options) {
107
166
  includeBucket ? cleanBucket : "",
108
167
  cleanFolder
109
168
  );
110
- if (!targetDir.startsWith(resolvedRoot)) {
111
- throw new Error("Path traversal blocked in LocalFsStorageAdapter");
112
- }
169
+ assertContainedInRoot(resolvedRoot, targetDir);
113
170
  try {
114
171
  const dirEntries = await fs.readdir(targetDir, { withFileTypes: true });
115
172
  const results = [];
@@ -124,10 +181,14 @@ function createLocalFsStorageAdapter(options) {
124
181
  size: stat.size
125
182
  });
126
183
  }
127
- return results;
184
+ const offset = listOptions?.cursor ? Number(listOptions.cursor) || 0 : 0;
185
+ const limit = listOptions?.limit ?? results.length;
186
+ const sliced = listOptions?.limit !== void 0 || offset > 0 ? results.slice(offset, offset + limit) : results;
187
+ const nextCursor = listOptions?.limit !== void 0 && offset + sliced.length < results.length ? String(offset + sliced.length) : void 0;
188
+ return toPaginatedListResult(sliced, nextCursor);
128
189
  } catch (err) {
129
190
  if (err.code === "ENOENT") {
130
- return [];
191
+ return toPaginatedListResult([]);
131
192
  }
132
193
  throw err;
133
194
  }
@@ -135,83 +196,6 @@ function createLocalFsStorageAdapter(options) {
135
196
  };
136
197
  }
137
198
 
138
- // src/storage/memoryAdapter.ts
139
- function createMemoryStorageAdapter(options = {}) {
140
- const baseUrl = (options.baseUrl || "https://memory.local/uploads").replace(
141
- /\/+$/,
142
- ""
143
- );
144
- const store = /* @__PURE__ */ new Map();
145
- const toKey = (bucket, path2) => `${bucket}::${path2.replace(/^\/+/, "")}`;
146
- const getPublicUrl = (bucket, path2) => {
147
- const cleanPath = path2.replace(/^\/+/, "");
148
- return `${baseUrl}/${bucket}/${cleanPath}`;
149
- };
150
- return {
151
- async upload(params) {
152
- const cleanPath = params.path.replace(/^\/+/, "");
153
- const key = toKey(params.bucket, cleanPath);
154
- if (params.upsert === false && store.has(key)) {
155
- throw new Error(`Object already exists at ${params.bucket}/${cleanPath}`);
156
- }
157
- store.set(key, {
158
- bucket: params.bucket,
159
- path: cleanPath,
160
- data: new Uint8Array(params.data),
161
- contentType: params.contentType,
162
- createdAt: (/* @__PURE__ */ new Date()).toISOString(),
163
- metadata: params.metadata
164
- });
165
- return {
166
- bucket: params.bucket,
167
- path: cleanPath,
168
- url: getPublicUrl(params.bucket, cleanPath)
169
- };
170
- },
171
- async delete(bucket, path2) {
172
- const key = toKey(bucket, path2);
173
- store.delete(key);
174
- },
175
- getPublicUrl,
176
- async list(bucket, folder) {
177
- const cleanFolder = folder ? folder.replace(/^\/+|\/+$/g, "") : "";
178
- const entries = [];
179
- for (const obj of store.values()) {
180
- if (obj.bucket !== bucket) continue;
181
- if (cleanFolder && !obj.path.startsWith(`${cleanFolder}/`)) continue;
182
- const name = cleanFolder ? obj.path.slice(cleanFolder.length + 1) : obj.path;
183
- entries.push({
184
- name,
185
- id: `${obj.bucket}/${obj.path}`,
186
- createdAt: obj.createdAt,
187
- size: obj.data.byteLength
188
- });
189
- }
190
- return entries;
191
- },
192
- async deleteByUrl(url) {
193
- const prefix = `${baseUrl}/`;
194
- if (!url.startsWith(prefix)) return false;
195
- const rest = url.slice(prefix.length);
196
- const slashIdx = rest.indexOf("/");
197
- if (slashIdx === -1) return false;
198
- const bucket = rest.slice(0, slashIdx);
199
- const filePath = rest.slice(slashIdx + 1);
200
- const key = toKey(bucket, filePath);
201
- return store.delete(key);
202
- },
203
- getObject(bucket, path2) {
204
- return store.get(toKey(bucket, path2));
205
- },
206
- clear() {
207
- store.clear();
208
- },
209
- get size() {
210
- return store.size;
211
- }
212
- };
213
- }
214
-
215
199
  // src/storage/s3Adapter.ts
216
200
  function normalizeS3Key(rawPath) {
217
201
  const normalized = rawPath.replace(/\\/g, "/").replace(/^\/+/, "");
@@ -224,6 +208,13 @@ function resolveTransport(options) {
224
208
  if ("putObject" in options && typeof options.putObject === "function") return options;
225
209
  throw new Error("S3StorageAdapter requires putObject/deleteObject or client/transport");
226
210
  }
211
+ function isS3PreconditionError(err) {
212
+ if (!err || typeof err !== "object") return false;
213
+ const e = err;
214
+ const status = e.$metadata?.httpStatusCode ?? Number(e.statusCode);
215
+ const code = String(e.code ?? e.name ?? "").toLowerCase();
216
+ return status === 412 || code === "preconditionfailed" || code === "eexist";
217
+ }
227
218
  function createS3StorageAdapter(options) {
228
219
  const transport = resolveTransport(options);
229
220
  const includeBucket = options.includeBucketInUrl !== false;
@@ -236,19 +227,34 @@ function createS3StorageAdapter(options) {
236
227
  return {
237
228
  async upload(params) {
238
229
  const key = normalizeS3Key(params.path);
239
- const cacheControl = params.cacheControl ?? options.cacheControl ?? (params.contentType.includes("image/avif") ? "public, max-age=31536000, immutable" : "public, max-age=2592000, must-revalidate");
230
+ validateStorageMetadata(params.metadata);
231
+ const isImmutable = params.upsert === false && params.contentType.includes("image/avif");
232
+ const cacheControl = params.cacheControl ?? options.cacheControl ?? (isImmutable ? "public, max-age=31536000, immutable" : "public, max-age=2592000, must-revalidate");
240
233
  const metadata = params.metadata || params.contentHash ? {
241
- ...params.contentHash ? { contentHash: params.contentHash } : {},
242
- ...params.metadata
234
+ ...params.metadata,
235
+ ...params.contentHash ? { contentHash: params.contentHash } : {}
243
236
  } : void 0;
244
- await transport.putObject({
245
- Bucket: params.bucket,
246
- Key: key,
247
- Body: params.data,
248
- ContentType: params.contentType,
249
- ...cacheControl ? { CacheControl: cacheControl } : {},
250
- ...metadata ? { Metadata: metadata } : {}
251
- });
237
+ try {
238
+ await transport.putObject({
239
+ Bucket: params.bucket,
240
+ Key: key,
241
+ Body: params.data,
242
+ ContentType: params.contentType,
243
+ ...cacheControl ? { CacheControl: cacheControl } : {},
244
+ ...metadata ? { Metadata: metadata } : {},
245
+ ...params.upsert === false ? { IfNoneMatch: "*" } : {}
246
+ });
247
+ } catch (err) {
248
+ if (isS3PreconditionError(err)) {
249
+ const existsErr = new Error(
250
+ `Object already exists at ${params.bucket}/${key}`
251
+ );
252
+ existsErr.code = "EEXIST";
253
+ existsErr.statusCode = 412;
254
+ throw existsErr;
255
+ }
256
+ throw err;
257
+ }
252
258
  return {
253
259
  bucket: params.bucket,
254
260
  path: key,
@@ -258,17 +264,34 @@ function createS3StorageAdapter(options) {
258
264
  async delete(bucket, path2) {
259
265
  await transport.deleteObject({ Bucket: bucket, Key: normalizeS3Key(path2) });
260
266
  },
267
+ async exists(bucket, path2) {
268
+ const key = normalizeS3Key(path2);
269
+ if (transport.headObject) {
270
+ try {
271
+ await transport.headObject({ Bucket: bucket, Key: key });
272
+ return true;
273
+ } catch {
274
+ return false;
275
+ }
276
+ }
277
+ const lastSlash = key.lastIndexOf("/");
278
+ const folder = lastSlash > 0 ? key.slice(0, lastSlash) : void 0;
279
+ const targetName = lastSlash > 0 ? key.slice(lastSlash + 1) : key;
280
+ const listed = await this.list(bucket, folder);
281
+ return listed.some((item) => item.name === targetName);
282
+ },
261
283
  getPublicUrl,
262
- async list(bucket, folder) {
284
+ async list(bucket, folder, listOptions) {
263
285
  const cleanFolder = folder ? normalizeS3Key(folder).replace(/\/+$/, "") : "";
264
286
  const prefix = cleanFolder ? `${cleanFolder}/` : void 0;
265
287
  const input = {
266
288
  Bucket: bucket,
267
289
  ...prefix ? { Prefix: prefix } : {},
268
- MaxKeys: 1e3
290
+ MaxKeys: listOptions?.limit ?? 1e3,
291
+ ...listOptions?.cursor ? { ContinuationToken: listOptions.cursor } : {}
269
292
  };
270
293
  const output = transport.listObjects ? await transport.listObjects(input) : transport.listObjectsV2 ? await transport.listObjectsV2(input) : void 0;
271
- return (output?.Contents ?? []).filter((item) => Boolean(item.Key)).map((item) => {
294
+ const entries = (output?.Contents ?? []).filter((item) => Boolean(item.Key)).map((item) => {
272
295
  const name = prefix && item.Key.startsWith(prefix) ? item.Key.slice(prefix.length) : item.Key;
273
296
  const createdAt = item.LastModified instanceof Date ? item.LastModified.toISOString() : item.LastModified;
274
297
  return {
@@ -278,26 +301,38 @@ function createS3StorageAdapter(options) {
278
301
  ...item.Size !== void 0 ? { size: item.Size } : {}
279
302
  };
280
303
  }).filter((entry) => entry.name.length > 0);
304
+ return toPaginatedListResult(entries, output?.NextContinuationToken);
281
305
  },
282
- async deleteByUrl(url) {
283
- if (typeof options.publicBaseUrl !== "string" || !url) return false;
306
+ parseUrl(url) {
307
+ if (typeof options.publicBaseUrl !== "string" || !url) return null;
284
308
  const prefix = `${options.publicBaseUrl.replace(/\/+$/, "")}/`;
285
309
  const cleanUrl = url.split(/[?#]/)[0];
286
- if (!cleanUrl.startsWith(prefix)) return false;
310
+ if (!cleanUrl.startsWith(prefix)) return null;
287
311
  try {
288
312
  const rest = decodeURIComponent(cleanUrl.slice(prefix.length)).replace(/^\/+/, "");
289
- if (!rest) return false;
313
+ if (!rest) return null;
290
314
  if (includeBucket) {
291
315
  const slashIdx = rest.indexOf("/");
292
- if (slashIdx <= 0 || slashIdx === rest.length - 1) return false;
293
- await transport.deleteObject({
294
- Bucket: rest.slice(0, slashIdx),
295
- Key: rest.slice(slashIdx + 1)
296
- });
297
- return true;
316
+ if (slashIdx <= 0 || slashIdx === rest.length - 1) return null;
317
+ return {
318
+ bucket: rest.slice(0, slashIdx),
319
+ path: rest.slice(slashIdx + 1)
320
+ };
298
321
  }
299
- if (!options.defaultBucket) return false;
300
- await transport.deleteObject({ Bucket: options.defaultBucket, Key: rest });
322
+ if (!options.defaultBucket) return null;
323
+ return { bucket: options.defaultBucket, path: rest };
324
+ } catch {
325
+ return null;
326
+ }
327
+ },
328
+ async deleteByUrl(url) {
329
+ const parsed = this.parseUrl?.(url);
330
+ if (!parsed || !parsed.bucket) return false;
331
+ try {
332
+ await transport.deleteObject({
333
+ Bucket: parsed.bucket,
334
+ Key: parsed.path
335
+ });
301
336
  return true;
302
337
  } catch {
303
338
  return false;
@@ -306,52 +341,22 @@ function createS3StorageAdapter(options) {
306
341
  };
307
342
  }
308
343
 
309
- // src/storage/urlParser.ts
310
- var DEFAULT_PUBLIC_STORAGE_PREFIX = "/storage/v1/object/public/";
311
- function parsePublicStorageUrl(url, publicPrefix = DEFAULT_PUBLIC_STORAGE_PREFIX) {
312
- if (!url) return null;
313
- try {
314
- const parsed = new URL(url, "http://localhost");
315
- const pathname = parsed.pathname;
316
- const prefixIndex = pathname.indexOf(publicPrefix);
317
- if (prefixIndex === -1) {
318
- return null;
319
- }
320
- const relativePath = pathname.slice(prefixIndex + publicPrefix.length).replace(/^\/+/, "");
321
- const segments = relativePath.split("/").filter(Boolean);
322
- if (segments.length < 2) {
323
- return null;
324
- }
325
- const [bucket, ...pathParts] = segments;
326
- const filePath = pathParts.join("/");
327
- const filename = pathParts[pathParts.length - 1];
328
- const lastDotIndex = filename.lastIndexOf(".");
329
- const extension = lastDotIndex >= 0 ? filename.slice(lastDotIndex) : null;
330
- return {
331
- bucket,
332
- path: filePath,
333
- filename,
334
- extension
335
- };
336
- } catch {
337
- return null;
338
- }
339
- }
340
-
341
344
  // src/storage/supabaseAdapter.ts
342
345
  function createSupabaseStorageAdapter(client, options = {}) {
343
346
  const publicPrefix = options.publicUrlPrefix || DEFAULT_PUBLIC_STORAGE_PREFIX;
344
347
  return {
345
348
  async upload(params) {
346
- const cacheControl = params.cacheControl || (params.contentType.includes("image/avif") ? "public, max-age=31536000, immutable" : "public, max-age=2592000, must-revalidate");
349
+ validateStorageMetadata(params.metadata);
350
+ const isImmutable = params.upsert === false && params.contentType.includes("image/avif");
351
+ const cacheControl = params.cacheControl || (isImmutable ? "public, max-age=31536000, immutable" : "public, max-age=2592000, must-revalidate");
347
352
  const { data, error } = await client.storage.from(params.bucket).upload(params.path, params.data, {
348
353
  contentType: params.contentType,
349
354
  upsert: params.upsert === void 0 ? true : params.upsert,
350
355
  cacheControl,
351
356
  metadata: {
357
+ ...params.metadata,
352
358
  uploadedAt: (/* @__PURE__ */ new Date()).toISOString(),
353
- contentHash: params.contentHash || "unknown",
354
- ...params.metadata
359
+ contentHash: params.contentHash || "unknown"
355
360
  }
356
361
  });
357
362
  if (error || !data) {
@@ -374,41 +379,53 @@ function createSupabaseStorageAdapter(client, options = {}) {
374
379
  throw new Error(`Failed to delete from Supabase storage: ${error.message}`);
375
380
  }
376
381
  },
382
+ async exists(bucket, path2) {
383
+ const cleanPath = path2.replace(/^\/+/, "");
384
+ const lastSlash = cleanPath.lastIndexOf("/");
385
+ const folder = lastSlash > 0 ? cleanPath.slice(0, lastSlash) : void 0;
386
+ const targetName = lastSlash > 0 ? cleanPath.slice(lastSlash + 1) : cleanPath;
387
+ const entries = await this.list(bucket, folder);
388
+ return entries.some((entry) => entry.name === targetName);
389
+ },
377
390
  getPublicUrl(bucket, path2) {
378
391
  const {
379
392
  data: { publicUrl }
380
393
  } = client.storage.from(bucket).getPublicUrl(path2);
381
394
  return publicUrl;
382
395
  },
383
- async list(bucket, folder) {
396
+ async list(bucket, folder, listOptions) {
384
397
  const bucketClient = client.storage.from(bucket);
385
398
  if (!bucketClient.list) {
386
- return [];
399
+ return toPaginatedListResult([]);
387
400
  }
401
+ const limit = listOptions?.limit ?? 1e3;
402
+ const offset = listOptions?.cursor ? Number(listOptions.cursor) || 0 : 0;
388
403
  const { data, error } = await bucketClient.list(folder, {
389
- limit: 1e3,
404
+ limit,
405
+ ...offset > 0 ? { offset } : {},
390
406
  sortBy: { column: "created_at", order: "desc" }
391
407
  });
392
408
  if (error) {
393
409
  throw new Error(`Failed to list Supabase bucket files: ${error.message}`);
394
410
  }
395
- return (data || []).filter((f) => f.id !== null).map((f) => ({
411
+ const entries = (data || []).filter((f) => f.id !== null).map((f) => ({
396
412
  name: f.name,
397
413
  id: f.id ?? void 0,
398
414
  createdAt: f.created_at ?? void 0
399
415
  }));
416
+ const nextCursor = listOptions?.limit !== void 0 && entries.length >= limit ? String(offset + entries.length) : void 0;
417
+ return toPaginatedListResult(entries, nextCursor);
418
+ },
419
+ parseUrl(url) {
420
+ const info = parsePublicStorageUrl(url, publicPrefix);
421
+ if (!info) return null;
422
+ return { bucket: info.bucket, path: info.path };
400
423
  },
401
424
  async deleteByUrl(url) {
402
425
  const info = parsePublicStorageUrl(url, publicPrefix);
403
426
  if (!info) return false;
404
427
  try {
405
- const targets = /* @__PURE__ */ new Set([info.path]);
406
- if (info.extension?.toLowerCase() === ".avif") {
407
- const basePath = info.path.slice(0, -info.extension.length);
408
- targets.add(`${basePath}-blur${info.extension}`);
409
- targets.add(`${basePath}-thumb${info.extension}`);
410
- }
411
- const { error } = await client.storage.from(info.bucket).remove([...targets]);
428
+ const { error } = await client.storage.from(info.bucket).remove([info.path]);
412
429
  return !error;
413
430
  } catch {
414
431
  return false;
@@ -416,12 +433,15 @@ function createSupabaseStorageAdapter(client, options = {}) {
416
433
  }
417
434
  };
418
435
  }
419
-
420
- // src/storage/types.ts
421
- function createStorageAdapter(adapter) {
422
- return adapter;
423
- }
424
-
425
- export { DEFAULT_PUBLIC_STORAGE_PREFIX, createLocalFsStorageAdapter, createMemoryStorageAdapter, createS3StorageAdapter, createStorageAdapter, createSupabaseStorageAdapter, parsePublicStorageUrl };
426
- //# sourceMappingURL=storage.js.map
436
+ export {
437
+ DEFAULT_PUBLIC_STORAGE_PREFIX,
438
+ createLocalFsStorageAdapter,
439
+ createMemoryStorageAdapter,
440
+ createS3StorageAdapter,
441
+ createStorageAdapter,
442
+ createSupabaseStorageAdapter,
443
+ parsePublicStorageUrl,
444
+ toPaginatedListResult,
445
+ validateStorageMetadata
446
+ };
427
447
  //# sourceMappingURL=storage.js.map