@stacksjs/storage 0.70.45 → 0.70.53

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/dist/index.js CHANGED
@@ -13,916 +13,2104 @@ var __export = (target, all) => {
13
13
  set: __exportSetter.bind(all, name)
14
14
  });
15
15
  };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
16
17
  var __require = import.meta.require;
17
18
 
18
- // src/copy.ts
19
- import { contains } from "@stacksjs/arrays";
20
- import { join } from "@stacksjs/path";
21
-
22
- // src/fs.ts
23
- import * as fs from "fs";
24
- import { existsSync, watch as fsWatch, mkdirSync, readFileSync, watchFile, writeFileSync } from "fs";
25
- function exists(path) {
26
- return existsSync(path);
27
- }
28
-
29
- // src/copy.ts
30
- function copy(src, dest, exclude = []) {
31
- if (Array.isArray(src)) {
32
- src.forEach((file) => {
33
- copy(file, dest, exclude);
34
- });
35
- } else {
36
- if (fs.statSync(src).isDirectory())
37
- copyFolder(src, dest, exclude);
38
- else
39
- copyFile(src, dest);
19
+ // src/types.ts
20
+ async function* createDirectoryListing(entries) {
21
+ for (const entry of entries) {
22
+ yield entry;
40
23
  }
41
24
  }
42
- function copyFile(src, dest) {
43
- fs.copyFileSync(src, dest);
44
- }
45
- function copyFolder(src, dest, exclude = []) {
46
- if (!fs.existsSync(dest))
47
- fs.mkdirSync(dest, { recursive: true });
48
- if (fs.existsSync(src)) {
49
- fs.readdirSync(src).forEach((file) => {
50
- if (!contains(join(src, file), exclude)) {
51
- const srcPath = join(src, file);
52
- const destPath = join(dest, file);
53
- if (fs.statSync(srcPath).isDirectory())
54
- copyFolder(srcPath, destPath, exclude);
55
- else
56
- fs.copyFileSync(srcPath, destPath);
57
- }
58
- });
25
+ function normalizeExpiryToMilliseconds(expiry) {
26
+ if (expiry instanceof Date) {
27
+ return expiry.getTime() - Date.now();
59
28
  }
29
+ return expiry * 1000;
60
30
  }
61
- // src/delete.ts
62
- import { italic, log } from "@stacksjs/cli";
63
- import { err, handleError, ok } from "@stacksjs/error-handling";
64
- import { join as join3 } from "@stacksjs/path";
65
-
66
- // src/folders.ts
67
- import { join as join2 } from "@stacksjs/path";
68
- function isFolder(path) {
69
- try {
70
- return fs.statSync(path).isDirectory();
71
- } catch {
72
- return false;
31
+ function normalizeExpiryToDate(expiry) {
32
+ if (expiry instanceof Date) {
33
+ return expiry;
73
34
  }
35
+ return new Date(Date.now() + expiry * 1000);
74
36
  }
75
- function isDir(path) {
76
- return isFolder(path);
77
- }
78
- function doesFolderExist(path) {
79
- return fs.existsSync(path);
80
- }
81
- function createFolder(dir) {
82
- return new Promise((resolve, reject) => {
83
- try {
84
- fs.mkdirSync(dir, { recursive: true });
85
- resolve();
86
- } catch (err) {
87
- reject(err);
88
- }
89
- });
37
+ function isFile2(entry) {
38
+ return entry.type === "file";
90
39
  }
91
- function getFolders(dir) {
92
- return fs.readdirSync(dir).filter((file) => {
93
- return fs.statSync(join2(dir, file)).isDirectory();
94
- });
40
+ function isDirectory(entry) {
41
+ return entry.type === "directory";
95
42
  }
96
- var folders = {
97
- isFolder,
98
- doesFolderExist,
99
- createFolder,
100
- getFolders
101
- };
43
+ var Visibility;
44
+ var init_types = __esm(() => {
45
+ ((Visibility2) => {
46
+ Visibility2["PUBLIC"] = "public";
47
+ Visibility2["PRIVATE"] = "private";
48
+ })(Visibility ||= {});
49
+ });
102
50
 
103
- // src/glob.ts
104
- var {Glob: BunGlob } = globalThis.Bun;
105
- function globSync(patterns, options) {
106
- const patternArray = typeof patterns === "string" ? [patterns] : patterns;
107
- const results = [];
108
- for (const pattern of patternArray) {
109
- const globInstance = new BunGlob(pattern);
110
- const matches = globInstance.scanSync({
111
- cwd: options?.cwd,
112
- absolute: options?.absolute,
113
- dot: options?.dot,
114
- onlyFiles: options?.onlyFiles
115
- });
116
- for (const match of matches) {
117
- results.push(match);
51
+ // src/signed-url.ts
52
+ import { Buffer } from "buffer";
53
+ import { createHmac, timingSafeEqual } from "crypto";
54
+ import process2 from "process";
55
+ function getAppKey() {
56
+ const k = process2.env.APP_KEY;
57
+ if (!k || k.length < 16) {
58
+ if (process2.env.APP_ENV === "production" || process2.env.NODE_ENV === "production") {
59
+ throw new Error("[storage/signed-url] APP_KEY is missing or too short (need \u226516 chars). Cannot sign URL.");
118
60
  }
119
61
  }
120
- return results;
62
+ return k || "stacks-default-key-dev-only-do-not-use-prod";
121
63
  }
122
- async function glob(patterns, options) {
123
- const patternArray = typeof patterns === "string" ? [patterns] : patterns;
124
- const results = [];
125
- for (const pattern of patternArray) {
126
- const globInstance = new BunGlob(pattern);
127
- const matches = globInstance.scan({
128
- cwd: options?.cwd,
129
- absolute: options?.absolute,
130
- dot: options?.dot,
131
- onlyFiles: options?.onlyFiles
132
- });
133
- for await (const match of matches) {
134
- results.push(match);
135
- }
136
- }
137
- return results;
64
+ function base64UrlEncode(buf) {
65
+ return buf.toString("base64url");
138
66
  }
139
-
140
- // src/delete.ts
141
- function deleteFolder(path) {
142
- return new Promise((resolve, reject) => {
143
- try {
144
- if (isFolder(path)) {
145
- fs.rmSync(path, { recursive: true, force: true });
146
- return resolve(ok(`Deleted ${path}`));
147
- }
148
- return resolve(ok(`Path ${path} was not a directory`));
149
- } catch (error) {
150
- return reject(err(error));
151
- }
152
- });
67
+ function base64UrlDecode(str) {
68
+ return Buffer.from(str, "base64url");
153
69
  }
154
- async function isDirectoryEmpty(path) {
155
- return new Promise((resolve, reject) => {
156
- try {
157
- if (fs.statSync(path).isDirectory()) {
158
- if (fs.readdirSync(path).length === 0)
159
- return resolve(ok(true));
160
- return resolve(ok(false));
161
- }
162
- return resolve(ok(false));
163
- } catch (error) {
164
- return reject(err(error));
165
- }
166
- });
70
+ function normalizeExpiry(expiresIn) {
71
+ if (expiresIn instanceof Date)
72
+ return Math.floor(expiresIn.getTime() / 1000);
73
+ return Math.floor(Date.now() / 1000) + Math.floor(expiresIn);
167
74
  }
168
- async function deleteEmptyFolder(path) {
169
- return new Promise((resolve, reject) => {
170
- try {
171
- if (fs.statSync(path).isDirectory()) {
172
- if (fs.readdirSync(path).length === 0) {
173
- fs.rmSync(path, { recursive: true, force: true });
174
- return resolve(ok(`Deleted ${path}`));
175
- }
176
- return resolve(ok(`Path ${path} was not empty`));
177
- }
178
- return resolve(ok(`Path ${path} was not a directory`));
179
- } catch (error) {
180
- return reject(err(error));
181
- }
182
- });
75
+ function createSignedStorageToken(path2, options) {
76
+ const exp = normalizeExpiry(options.expiresIn);
77
+ const iat = Math.floor(Date.now() / 1000);
78
+ const header = { alg: ALG, typ: "JWT" };
79
+ const payload = {
80
+ iss: options.issuer || "stacks",
81
+ iat,
82
+ exp,
83
+ path: path2
84
+ };
85
+ const headerPart = base64UrlEncode(Buffer.from(JSON.stringify(header)));
86
+ const payloadPart = base64UrlEncode(Buffer.from(JSON.stringify(payload)));
87
+ const signingInput = `${headerPart}.${payloadPart}`;
88
+ const sig = base64UrlEncode(createHmac("sha256", getAppKey()).update(signingInput).digest());
89
+ return `${signingInput}.${sig}`;
183
90
  }
184
- async function deleteEmptyFolders(dir) {
185
- try {
186
- if (!fs.existsSync(dir))
187
- return ok(`Path ${dir} does not exist`);
188
- const files = fs.readdirSync(dir);
189
- for (const file of files) {
190
- const p = join3(dir, file);
191
- if (isFolder(p)) {
192
- if (fs.readdirSync(p).length === 0)
193
- fs.rmSync(p, { recursive: true, force: true });
194
- else
195
- await deleteEmptyFolders(p);
196
- }
197
- }
198
- return ok(`Deleted empty folders located in ${dir}`);
199
- } catch (error) {
200
- return err(error);
91
+ function rememberRevoked(sigPart) {
92
+ if (revokedSignatures.has(sigPart))
93
+ return;
94
+ if (revokedSignatures.size >= REVOCATION_LIMIT) {
95
+ const oldest = revokedSignatures.values().next().value;
96
+ if (oldest !== undefined)
97
+ revokedSignatures.delete(oldest);
201
98
  }
99
+ revokedSignatures.add(sigPart);
202
100
  }
203
- function deleteFile(path) {
204
- return new Promise((resolve, reject) => {
205
- try {
206
- if (fs.statSync(path).isFile()) {
207
- fs.rmSync(path, { recursive: true, force: true });
208
- return resolve(ok(`Deleted ${path}`));
209
- }
210
- return resolve(ok(`Path ${path} was not a file`));
211
- } catch (error) {
212
- return reject(err(error));
213
- }
214
- });
101
+ function revokeSignedStorageToken(token) {
102
+ if (typeof token !== "string" || token.length === 0)
103
+ return;
104
+ const parts = token.split(".");
105
+ const sig = parts.length === 3 ? parts[2] : token;
106
+ if (sig)
107
+ rememberRevoked(sig);
215
108
  }
216
- async function deleteGlob(path) {
217
- if (!path.includes("*"))
218
- return err(handleError(`Path ${path} does not contain a glob`));
219
- const directories = await glob([path], { onlyDirectories: true });
220
- for (const directory of directories) {
221
- const result = await deleteFolder(directory);
222
- if (result.isErr) {
223
- log.error(result.error);
224
- return result;
225
- }
226
- log.info(`Deleted ${italic(directory)}`);
227
- }
228
- return ok(`Deleted ${directories.length} directories`);
109
+ function isSignedStorageTokenRevoked(sigPart) {
110
+ return revokedSignatures.has(sigPart);
229
111
  }
230
- async function del(path) {
231
- if (fs.existsSync(path) && fs.statSync(path).isFile())
232
- return await deleteFile(path);
233
- if (isFolder(path))
234
- return await deleteFolder(path);
235
- if (path.includes("*"))
236
- return await deleteGlob(path);
237
- return err(handleError(`Path ${path} cannot be deleted due to an unhandled condition. Please report this issue.`));
112
+ function clearRevokedSignedStorageTokens() {
113
+ revokedSignatures.clear();
238
114
  }
239
- // src/files.ts
240
- import { contains as contains2 } from "@stacksjs/arrays";
241
- import { log as log2 } from "@stacksjs/logging";
242
- import { dirname, join as join4, path as p } from "@stacksjs/path";
243
- import { detectIndent, detectNewline } from "@stacksjs/strings";
244
- async function readJsonFile(name, cwd) {
245
- const file = await readTextFile(name, cwd);
246
- let data;
247
- try {
248
- data = JSON.parse(file.data);
249
- } catch (error) {
250
- throw new Error(`Failed to parse JSON file "${name}": ${error.message}`);
115
+ function verifySignedStorageToken(token, requestedPath) {
116
+ if (typeof token !== "string") {
117
+ return { valid: false, reason: "malformed" };
251
118
  }
252
- const indent = detectIndent(file.data).indent;
253
- const newline = detectNewline(file.data);
254
- return { ...file, data, indent, newline };
119
+ const parts = token.split(".");
120
+ if (parts.length !== 3) {
121
+ return { valid: false, reason: "malformed" };
122
+ }
123
+ const headerPart = parts[0];
124
+ const payloadPart = parts[1];
125
+ const sigPart = parts[2];
126
+ const signingInput = `${headerPart}.${payloadPart}`;
127
+ const expectedSig = createHmac("sha256", getAppKey()).update(signingInput).digest();
128
+ let providedSig;
129
+ try {
130
+ providedSig = base64UrlDecode(sigPart);
131
+ } catch {
132
+ return { valid: false, reason: "malformed" };
133
+ }
134
+ if (providedSig.length !== expectedSig.length || !timingSafeEqual(providedSig, expectedSig)) {
135
+ return { valid: false, reason: "bad_signature" };
136
+ }
137
+ if (revokedSignatures.has(sigPart)) {
138
+ return { valid: false, reason: "revoked" };
139
+ }
140
+ let claims;
141
+ try {
142
+ claims = JSON.parse(base64UrlDecode(payloadPart).toString("utf8"));
143
+ } catch {
144
+ return { valid: false, reason: "malformed" };
145
+ }
146
+ const now = Math.floor(Date.now() / 1000);
147
+ if (typeof claims.exp !== "number" || now >= claims.exp) {
148
+ return { valid: false, reason: "expired" };
149
+ }
150
+ if (claims.path !== requestedPath) {
151
+ return { valid: false, reason: "path_mismatch" };
152
+ }
153
+ return { valid: true, claims };
255
154
  }
256
- async function readPackageJson(name, cwd) {
257
- const file = await readJsonFile(name, cwd);
258
- return file.data;
155
+ var ALG = "HS256", REVOCATION_LIMIT = 1e5, revokedSignatures;
156
+ var init_signed_url = __esm(() => {
157
+ revokedSignatures = new Set;
158
+ });
159
+
160
+ // src/adapters/local.ts
161
+ import { Buffer as Buffer2 } from "buffer";
162
+ import { createHmac as createHmac2 } from "crypto";
163
+ import { createReadStream, createWriteStream } from "fs";
164
+ import { access, chmod, constants, copyFile as copyFile2, lstat, mkdir, readdir, readFile, rename as rename2, rm, stat, unlink, writeFile as writeFile2 } from "fs/promises";
165
+ import { basename, dirname as dirname3, join as join5, relative } from "path";
166
+ import { Readable } from "stream";
167
+ import { pipeline } from "stream/promises";
168
+
169
+ class LocalStorageAdapter {
170
+ root;
171
+ constructor(config = {}) {
172
+ this.root = config.root || process.cwd();
173
+ }
174
+ resolvePath(path2) {
175
+ const resolved = join5(this.root, path2);
176
+ const rel = relative(this.root, resolved);
177
+ if (rel.startsWith("..") || rel.startsWith("../") || rel.startsWith("..\\")) {
178
+ throw new Error(`Path traversal detected: '${path2}' resolves outside storage root`);
179
+ }
180
+ return resolved;
181
+ }
182
+ async write(path2, contents) {
183
+ const fullPath = this.resolvePath(path2);
184
+ const dir = dirname3(fullPath);
185
+ await mkdir(dir, { recursive: true });
186
+ if (typeof contents === "string") {
187
+ await writeFile2(fullPath, contents, "utf8");
188
+ } else if (contents instanceof Buffer2) {
189
+ await writeFile2(fullPath, contents);
190
+ } else if (contents instanceof Uint8Array) {
191
+ await writeFile2(fullPath, contents);
192
+ } else {
193
+ const writeStream = createWriteStream(fullPath);
194
+ await pipeline(contents, writeStream);
195
+ }
196
+ const st = await stat(fullPath);
197
+ return {
198
+ path: path2,
199
+ size: st.size,
200
+ lastModified: st.mtimeMs
201
+ };
202
+ }
203
+ async read(path2) {
204
+ const fullPath = this.resolvePath(path2);
205
+ return await readFile(fullPath);
206
+ }
207
+ async getStream(path2, options) {
208
+ const fullPath = this.resolvePath(path2);
209
+ await access(fullPath, constants.R_OK);
210
+ const nodeStream = createReadStream(fullPath, { signal: options?.signal });
211
+ return Readable.toWeb(nodeStream);
212
+ }
213
+ async putStream(path2, stream, options) {
214
+ const fullPath = this.resolvePath(path2);
215
+ const dir = dirname3(fullPath);
216
+ await mkdir(dir, { recursive: true });
217
+ const nodeReadable = Readable.fromWeb(stream);
218
+ const writeStream = createWriteStream(fullPath);
219
+ try {
220
+ await pipeline(nodeReadable, writeStream, { signal: options?.signal });
221
+ } catch (err3) {
222
+ try {
223
+ await unlink(fullPath);
224
+ } catch {}
225
+ throw err3;
226
+ }
227
+ const st = await stat(fullPath);
228
+ return {
229
+ path: path2,
230
+ size: st.size,
231
+ contentType: options?.contentType,
232
+ lastModified: st.mtimeMs
233
+ };
234
+ }
235
+ async readToString(path2) {
236
+ const fullPath = this.resolvePath(path2);
237
+ return await readFile(fullPath, "utf8");
238
+ }
239
+ async readToBuffer(path2) {
240
+ const fullPath = this.resolvePath(path2);
241
+ return await readFile(fullPath);
242
+ }
243
+ async readToUint8Array(path2) {
244
+ const fullPath = this.resolvePath(path2);
245
+ const buffer = await readFile(fullPath);
246
+ return new Uint8Array(buffer);
247
+ }
248
+ async deleteFile(path2) {
249
+ const fullPath = this.resolvePath(path2);
250
+ await unlink(fullPath);
251
+ }
252
+ async deleteDirectory(path2) {
253
+ const fullPath = this.resolvePath(path2);
254
+ await rm(fullPath, { recursive: true, force: true });
255
+ }
256
+ async createDirectory(path2) {
257
+ const fullPath = this.resolvePath(path2);
258
+ await mkdir(fullPath, { recursive: true });
259
+ }
260
+ async moveFile(from, to) {
261
+ const fromPath = this.resolvePath(from);
262
+ const toPath = this.resolvePath(to);
263
+ const toDir = dirname3(toPath);
264
+ await mkdir(toDir, { recursive: true });
265
+ await rename2(fromPath, toPath);
266
+ }
267
+ async copyFile(from, to) {
268
+ const fromPath = this.resolvePath(from);
269
+ const toPath = this.resolvePath(to);
270
+ const toDir = dirname3(toPath);
271
+ await mkdir(toDir, { recursive: true });
272
+ await copyFile2(fromPath, toPath);
273
+ }
274
+ async stat(path2) {
275
+ const fullPath = this.resolvePath(path2);
276
+ const stats = await lstat(fullPath);
277
+ return {
278
+ path: path2,
279
+ type: stats.isDirectory() ? "directory" : "file",
280
+ visibility: await this.visibility(path2),
281
+ size: stats.size,
282
+ lastModified: stats.mtimeMs,
283
+ mimeType: stats.isFile() ? await this.detectMimeType(fullPath) : undefined
284
+ };
285
+ }
286
+ list(path2, options = {}) {
287
+ return this.createAsyncIterator(path2, options.deep || false);
288
+ }
289
+ async* createAsyncIterator(path2, deep) {
290
+ const fullPath = this.resolvePath(path2);
291
+ try {
292
+ await access(fullPath, constants.R_OK);
293
+ } catch {
294
+ return;
295
+ }
296
+ const entries = await this.readDirectoryRecursive(fullPath, deep);
297
+ yield* createDirectoryListing(entries);
298
+ }
299
+ async readDirectoryRecursive(dirPath, deep) {
300
+ const entries = [];
301
+ try {
302
+ const items = await readdir(dirPath, { withFileTypes: true });
303
+ for (const item of items) {
304
+ const itemPath = join5(dirPath, item.name);
305
+ const relativePath = relative(this.root, itemPath);
306
+ entries.push({
307
+ path: relativePath,
308
+ type: item.isDirectory() ? "directory" : "file"
309
+ });
310
+ if (deep && item.isDirectory()) {
311
+ const subEntries = await this.readDirectoryRecursive(itemPath, true);
312
+ entries.push(...subEntries);
313
+ }
314
+ }
315
+ } catch (error) {
316
+ if (error?.code !== "EACCES" && error?.code !== "EPERM") {
317
+ throw error;
318
+ }
319
+ }
320
+ return entries;
321
+ }
322
+ async changeVisibility(path2, vis) {
323
+ const fullPath = this.resolvePath(path2);
324
+ const stats = await lstat(fullPath);
325
+ const isDir2 = stats.isDirectory();
326
+ const mode = vis === "public" ? isDir2 ? 493 : 420 : isDir2 ? 448 : 384;
327
+ await chmod(fullPath, mode);
328
+ }
329
+ async visibility(path2) {
330
+ const fullPath = this.resolvePath(path2);
331
+ const stats = await lstat(fullPath);
332
+ const perms = stats.mode & 511;
333
+ return perms & 4 ? "public" : "private";
334
+ }
335
+ async fileExists(path2) {
336
+ const fullPath = this.resolvePath(path2);
337
+ try {
338
+ const stats = await lstat(fullPath);
339
+ return stats.isFile();
340
+ } catch {
341
+ return false;
342
+ }
343
+ }
344
+ async directoryExists(path2) {
345
+ const fullPath = this.resolvePath(path2);
346
+ try {
347
+ const stats = await lstat(fullPath);
348
+ return stats.isDirectory();
349
+ } catch {
350
+ return false;
351
+ }
352
+ }
353
+ async publicUrl(path2, options = {}) {
354
+ const base = (options.domain || process.env.APP_URL || "http://localhost").replace(/\/$/, "");
355
+ return `${base}/${path2}`;
356
+ }
357
+ async temporaryUrl(path2, options) {
358
+ const expiry = normalizeExpiryToDate(options.expiresIn);
359
+ const payload = `${path2}:${expiry.getTime()}`;
360
+ const appKey = process.env.APP_KEY || "stacks-default-key";
361
+ const signature = createHmac2("sha256", appKey).update(payload).digest("hex");
362
+ const token = Buffer2.from(`${payload}:${signature}`).toString("base64url");
363
+ return `http://localhost/temp/${token}`;
364
+ }
365
+ async signedUrl(path2, options) {
366
+ const token = createSignedStorageToken(path2, options);
367
+ const baseUrl = (options.baseUrl || process.env.APP_URL || "http://localhost").replace(/\/$/, "");
368
+ return `${baseUrl}/__storage/${encodeURIComponent(path2)}?token=${token}`;
369
+ }
370
+ async checksum(path2, options = {}) {
371
+ const algorithm = options.algorithm || "sha256";
372
+ const fullPath = this.resolvePath(path2);
373
+ const content = await readFile(fullPath);
374
+ const hasher = new Bun.CryptoHasher(algorithm);
375
+ hasher.update(content);
376
+ return hasher.digest("hex");
377
+ }
378
+ async mimeType(path2, options = {}) {
379
+ const fullPath = this.resolvePath(path2);
380
+ return await this.detectMimeType(fullPath);
381
+ }
382
+ async detectMimeType(filePath) {
383
+ const ext = basename(filePath).split(".").pop()?.toLowerCase();
384
+ const mimeTypes = {
385
+ txt: "text/plain",
386
+ html: "text/html",
387
+ css: "text/css",
388
+ js: "application/javascript",
389
+ json: "application/json",
390
+ xml: "application/xml",
391
+ pdf: "application/pdf",
392
+ zip: "application/zip",
393
+ jpg: "image/jpeg",
394
+ jpeg: "image/jpeg",
395
+ png: "image/png",
396
+ gif: "image/gif",
397
+ svg: "image/svg+xml",
398
+ mp4: "video/mp4",
399
+ mp3: "audio/mpeg",
400
+ wav: "audio/wav"
401
+ };
402
+ return mimeTypes[ext || ""] || "application/octet-stream";
403
+ }
404
+ async lastModified(path2) {
405
+ const stats = await this.stat(path2);
406
+ return stats.lastModified;
407
+ }
408
+ async fileSize(path2) {
409
+ const stats = await this.stat(path2);
410
+ return stats.size;
411
+ }
259
412
  }
260
- async function writeFile(path, data) {
261
- if (typeof path === "string") {
262
- const dirPath = dirname(path);
263
- if (!await existsSync(dirPath))
264
- await createFolder(dirPath);
265
- return await Bun.write(Bun.file(path), data);
413
+ function createLocalStorage(config = {}) {
414
+ return new LocalStorageAdapter(config);
415
+ }
416
+ var init_local = __esm(() => {
417
+ init_types();
418
+ init_signed_url();
419
+ });
420
+
421
+ // src/path-sanitize.ts
422
+ function sanitizePresignedDir(dir) {
423
+ if (dir === undefined || dir === "")
424
+ return "";
425
+ if (typeof dir !== "string")
426
+ throw new PathSanitizeError(`dir must be a string, got ${typeof dir}`, "not-string");
427
+ const trimmed = dir.replace(/^\/+/, "").replace(/\/+$/, "");
428
+ if (dir.startsWith("/"))
429
+ throw new PathSanitizeError(`dir must not be absolute: '${dir}'`, "absolute-path");
430
+ if (trimmed === "")
431
+ return "";
432
+ if (trimmed.includes("\x00"))
433
+ throw new PathSanitizeError(`dir contains null byte`, "null-byte");
434
+ if (/[\x00-\x1F\x7F]/.test(trimmed))
435
+ throw new PathSanitizeError(`dir contains control character`, "control-char");
436
+ const segments = trimmed.split("/");
437
+ for (const segment of segments) {
438
+ if (segment === "" || segment === "." || segment === "..")
439
+ throw new PathSanitizeError(`dir contains traversal or empty segment: '${dir}'`, "traversal");
440
+ if (segment.length > MAX_COMPONENT_LENGTH)
441
+ throw new PathSanitizeError(`dir segment exceeds ${MAX_COMPONENT_LENGTH} chars`, "too-long");
442
+ if (!ALLOWED_DIR_CHAR.test(segment))
443
+ throw new PathSanitizeError(`dir segment contains disallowed character: '${segment}'`, "invalid-char");
444
+ }
445
+ return segments.join("/");
446
+ }
447
+ function sanitizePresignedFilename(filename) {
448
+ if (typeof filename !== "string")
449
+ throw new PathSanitizeError(`filename must be a string, got ${typeof filename}`, "not-string");
450
+ if (filename === "")
451
+ throw new PathSanitizeError(`filename must not be empty`, "empty");
452
+ if (filename.length > MAX_COMPONENT_LENGTH)
453
+ throw new PathSanitizeError(`filename exceeds ${MAX_COMPONENT_LENGTH} chars`, "too-long");
454
+ if (filename.includes("\x00"))
455
+ throw new PathSanitizeError(`filename contains null byte`, "null-byte");
456
+ if (/[\x00-\x1F\x7F]/.test(filename))
457
+ throw new PathSanitizeError(`filename contains control character`, "control-char");
458
+ if (filename.includes("/") || filename.includes("\\"))
459
+ throw new PathSanitizeError(`filename must not contain path separators: '${filename}'`, "traversal");
460
+ if (filename === "." || filename === ".." || filename.startsWith("../") || filename.includes("/.."))
461
+ throw new PathSanitizeError(`filename contains traversal token: '${filename}'`, "traversal");
462
+ if (!ALLOWED_FILENAME_CHAR.test(filename))
463
+ throw new PathSanitizeError(`filename contains disallowed character: '${filename}'`, "invalid-char");
464
+ const dotIdx = filename.lastIndexOf(".");
465
+ if (dotIdx > 0 && dotIdx < filename.length - 1) {
466
+ const ext = filename.slice(dotIdx + 1).toLowerCase();
467
+ if (!ALLOWED_EXTENSION.test(ext))
468
+ throw new PathSanitizeError(`filename has invalid extension: '.${ext}'`, "invalid-extension");
469
+ }
470
+ return filename;
471
+ }
472
+ function parseDiskPath(input) {
473
+ if (typeof input !== "string" || input.length === 0) {
474
+ throw new PathSanitizeError("disk-path reference is empty", "empty");
475
+ }
476
+ if (input.includes("\x00")) {
477
+ throw new PathSanitizeError("disk-path reference contains a null byte", "null-byte");
478
+ }
479
+ const colonIdx = input.indexOf(":");
480
+ if (colonIdx <= 0 || colonIdx === input.length - 1) {
481
+ throw new PathSanitizeError(`disk-path reference must use '<disk>:<path>' format, got '${input}'`, "invalid-char");
482
+ }
483
+ const disk = input.slice(0, colonIdx);
484
+ const path2 = input.slice(colonIdx + 1);
485
+ if (!DISK_NAME_RE.test(disk)) {
486
+ throw new PathSanitizeError(`disk name '${disk}' is invalid (alphanumeric + '-' / '_' only)`, "invalid-char");
487
+ }
488
+ if (path2.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path2)) {
489
+ throw new PathSanitizeError(`disk-path '${path2}' is absolute`, "absolute-path");
490
+ }
491
+ if (path2.includes("\x00")) {
492
+ throw new PathSanitizeError("disk-path contains a null byte", "null-byte");
493
+ }
494
+ for (let i = 0;i < path2.length; i++) {
495
+ const code = path2.charCodeAt(i);
496
+ if (code < 32 || code === 127) {
497
+ throw new PathSanitizeError(`disk-path contains a control character at index ${i}`, "control-char");
498
+ }
499
+ }
500
+ const segments = path2.split(/[/\\]/);
501
+ if (segments.some((seg) => seg === "..")) {
502
+ throw new PathSanitizeError(`disk-path '${path2}' contains a '..' segment`, "traversal");
503
+ }
504
+ return { disk, path: path2 };
505
+ }
506
+ var PathSanitizeError, MAX_COMPONENT_LENGTH = 255, ALLOWED_DIR_CHAR, ALLOWED_FILENAME_CHAR, ALLOWED_EXTENSION, DISK_NAME_RE;
507
+ var init_path_sanitize = __esm(() => {
508
+ PathSanitizeError = class PathSanitizeError extends Error {
509
+ reason;
510
+ constructor(message, reason) {
511
+ super(message);
512
+ this.name = "PathSanitizeError";
513
+ this.reason = reason;
514
+ }
515
+ };
516
+ ALLOWED_DIR_CHAR = /^[A-Za-z0-9._-]+$/;
517
+ ALLOWED_FILENAME_CHAR = /^[A-Za-z0-9._-]+$/;
518
+ ALLOWED_EXTENSION = /^[a-z0-9]+$/;
519
+ DISK_NAME_RE = /^[a-z0-9_-]+$/i;
520
+ });
521
+
522
+ // src/s3-presigned-post.ts
523
+ import { createHmac as createHmac3 } from "crypto";
524
+ import { Buffer as Buffer4 } from "buffer";
525
+ function hmac(key, data) {
526
+ return createHmac3("sha256", key).update(data, "utf8").digest();
527
+ }
528
+ function deriveSigningKey(secretAccessKey, dateStamp, region) {
529
+ const kDate = hmac(`AWS4${secretAccessKey}`, dateStamp);
530
+ const kRegion = hmac(kDate, region);
531
+ const kService = hmac(kRegion, "s3");
532
+ const kSigning = hmac(kService, "aws4_request");
533
+ return kSigning;
534
+ }
535
+ function isoDate(now) {
536
+ const amzDate = now.toISOString().replace(/[:-]|\.\d{3}/g, "");
537
+ const dateStamp = amzDate.slice(0, 8);
538
+ return { amzDate, dateStamp };
539
+ }
540
+ function signS3PresignedPost(input) {
541
+ const expiresIn = Math.floor(input.expiresIn);
542
+ if (!Number.isFinite(expiresIn) || expiresIn < MIN_EXPIRY || expiresIn > MAX_EXPIRY) {
543
+ throw new RangeError(`[storage/s3-post] expiresIn must be between ${MIN_EXPIRY}s and ${MAX_EXPIRY}s (got ${expiresIn}s)`);
544
+ }
545
+ if (!input.bucket)
546
+ throw new Error("[storage/s3-post] bucket is required");
547
+ if (!input.credentials?.accessKeyId || !input.credentials?.secretAccessKey) {
548
+ throw new Error("[storage/s3-post] credentials.accessKeyId and credentials.secretAccessKey are required");
549
+ }
550
+ const now = new Date;
551
+ const { amzDate, dateStamp } = isoDate(now);
552
+ const credentialScope = `${dateStamp}/${input.region}/s3/aws4_request`;
553
+ const credentialField = `${input.credentials.accessKeyId}/${credentialScope}`;
554
+ const expirationDate = new Date(now.getTime() + expiresIn * 1000).toISOString().replace(/\.\d{3}Z$/, "Z");
555
+ const conditions = [];
556
+ conditions.push({ bucket: input.bucket });
557
+ if (typeof input.key === "string") {
558
+ conditions.push({ key: input.key });
559
+ } else {
560
+ conditions.push(["starts-with", "$key", input.key.startsWith]);
561
+ }
562
+ const acl = input.acl ?? "private";
563
+ conditions.push({ acl });
564
+ if (typeof input.contentType === "string") {
565
+ conditions.push({ "Content-Type": input.contentType });
566
+ } else {
567
+ conditions.push(["starts-with", "$Content-Type", input.contentType.startsWith]);
568
+ }
569
+ if (input.contentLengthRange) {
570
+ if (!Number.isFinite(input.contentLengthRange.min) || input.contentLengthRange.min < 0 || !Number.isFinite(input.contentLengthRange.max) || input.contentLengthRange.max < input.contentLengthRange.min) {
571
+ throw new RangeError("[storage/s3-post] contentLengthRange must satisfy 0 <= min <= max");
572
+ }
573
+ conditions.push(["content-length-range", input.contentLengthRange.min, input.contentLengthRange.max]);
574
+ }
575
+ if (input.fields) {
576
+ for (const [k, v] of Object.entries(input.fields)) {
577
+ conditions.push({ [k]: v });
578
+ }
579
+ }
580
+ conditions.push({ "x-amz-credential": credentialField });
581
+ conditions.push({ "x-amz-algorithm": ALGORITHM });
582
+ conditions.push({ "x-amz-date": amzDate });
583
+ if (input.credentials.sessionToken) {
584
+ conditions.push({ "x-amz-security-token": input.credentials.sessionToken });
585
+ }
586
+ const policy = {
587
+ expiration: expirationDate,
588
+ conditions
589
+ };
590
+ const policyBase64 = Buffer4.from(JSON.stringify(policy), "utf8").toString("base64");
591
+ const signingKey = deriveSigningKey(input.credentials.secretAccessKey, dateStamp, input.region);
592
+ const signature = createHmac3("sha256", signingKey).update(policyBase64, "utf8").digest("hex");
593
+ const fields = {
594
+ key: typeof input.key === "string" ? input.key : `${input.key.startsWith}\${filename}`,
595
+ acl,
596
+ "Content-Type": typeof input.contentType === "string" ? input.contentType : input.contentType.startsWith,
597
+ "x-amz-credential": credentialField,
598
+ "x-amz-algorithm": ALGORITHM,
599
+ "x-amz-date": amzDate,
600
+ policy: policyBase64,
601
+ "x-amz-signature": signature,
602
+ ...input.credentials.sessionToken ? { "x-amz-security-token": input.credentials.sessionToken } : {},
603
+ ...input.fields ?? {}
604
+ };
605
+ return {
606
+ url: `https://${input.bucket}.s3.${input.region}.amazonaws.com/`,
607
+ fields,
608
+ key: typeof input.key === "string" ? input.key : input.key.startsWith
609
+ };
610
+ }
611
+ var ALGORITHM = "AWS4-HMAC-SHA256", MIN_EXPIRY = 60, MAX_EXPIRY;
612
+ var init_s3_presigned_post = __esm(() => {
613
+ MAX_EXPIRY = 7 * 24 * 60 * 60;
614
+ });
615
+
616
+ // src/adapters/s3.ts
617
+ import { Buffer as Buffer5 } from "buffer";
618
+ import { basename as basename3 } from "path";
619
+ import process3 from "process";
620
+ function clampPartSize(requested) {
621
+ if (!Number.isFinite(requested))
622
+ return S3_MIN_PART_SIZE;
623
+ return Math.max(S3_MIN_PART_SIZE, Math.min(Math.floor(requested), S3_MAX_PART_SIZE));
624
+ }
625
+
626
+ class ChunkBuffer {
627
+ chunks = [];
628
+ total = 0;
629
+ constructor(_partSize) {}
630
+ get length() {
631
+ return this.total;
632
+ }
633
+ push(c) {
634
+ this.chunks.push(c);
635
+ this.total += c.length;
636
+ }
637
+ take(n) {
638
+ const out = new Uint8Array(n);
639
+ let written = 0;
640
+ while (written < n && this.chunks.length > 0) {
641
+ const head = this.chunks[0];
642
+ const need = n - written;
643
+ if (head.length <= need) {
644
+ out.set(head, written);
645
+ written += head.length;
646
+ this.chunks.shift();
647
+ } else {
648
+ out.set(head.subarray(0, need), written);
649
+ this.chunks[0] = head.subarray(need);
650
+ written += need;
651
+ }
652
+ }
653
+ this.total -= n;
654
+ return out;
655
+ }
656
+ flush() {
657
+ const out = new Uint8Array(this.total);
658
+ let off = 0;
659
+ for (const c of this.chunks) {
660
+ out.set(c, off);
661
+ off += c.length;
662
+ }
663
+ this.chunks = [];
664
+ this.total = 0;
665
+ return out;
666
+ }
667
+ }
668
+ async function isSettled(p3) {
669
+ const sentinel = Symbol("pending");
670
+ const result = await Promise.race([
671
+ p3.then(() => "settled", () => "settled"),
672
+ Promise.resolve(sentinel)
673
+ ]);
674
+ return result !== sentinel;
675
+ }
676
+
677
+ class S3StorageAdapter {
678
+ _client;
679
+ _clientPromise = null;
680
+ bucket;
681
+ prefix;
682
+ region;
683
+ credentials;
684
+ constructor(client, config) {
685
+ this._client = client;
686
+ this.bucket = config.bucket || "";
687
+ this.prefix = config.prefix || "";
688
+ this.region = config.region || "us-east-1";
689
+ this.credentials = config.credentials;
690
+ if (!this.bucket) {
691
+ throw new Error("S3 bucket name is required");
692
+ }
693
+ }
694
+ async getClient() {
695
+ if (this._client)
696
+ return this._client;
697
+ if (!this._clientPromise) {
698
+ this._clientPromise = import("@stacksjs/ts-cloud").then((cloud) => {
699
+ this._client = new cloud.S3Client(this.region);
700
+ return this._client;
701
+ });
702
+ }
703
+ return this._clientPromise;
704
+ }
705
+ resolveCredentials() {
706
+ if (this.credentials?.accessKeyId && this.credentials.secretAccessKey)
707
+ return this.credentials;
708
+ const accessKeyId = process3.env.AWS_ACCESS_KEY_ID;
709
+ const secretAccessKey = process3.env.AWS_SECRET_ACCESS_KEY;
710
+ const sessionToken = process3.env.AWS_SESSION_TOKEN;
711
+ if (!accessKeyId || !secretAccessKey) {
712
+ throw new Error("[storage/s3] presignedUploadPolicy requires AWS credentials \u2014 " + "pass them via S3DiskConfig.credentials or set AWS_ACCESS_KEY_ID + AWS_SECRET_ACCESS_KEY.");
713
+ }
714
+ return { accessKeyId, secretAccessKey, sessionToken };
715
+ }
716
+ prefixPath(path2) {
717
+ if (!this.prefix)
718
+ return path2;
719
+ return `${this.prefix}/${path2}`.replace(/\/+/g, "/");
720
+ }
721
+ stripPrefix(path2) {
722
+ if (!this.prefix)
723
+ return path2;
724
+ const prefixWithSlash = `${this.prefix}/`;
725
+ return path2.startsWith(prefixWithSlash) ? path2.slice(prefixWithSlash.length) : path2;
726
+ }
727
+ async contentsToBuffer(contents) {
728
+ if (typeof contents === "string") {
729
+ return Buffer5.from(contents, "utf8");
730
+ } else if (contents instanceof Buffer5) {
731
+ return contents;
732
+ } else if (contents instanceof Uint8Array) {
733
+ return Buffer5.from(contents);
734
+ } else {
735
+ const stream = contents;
736
+ if (typeof stream.getReader !== "function") {
737
+ throw new TypeError("[storage/s3] contents must be a web-standard ReadableStream (with .getReader()), not a Node stream.Readable. Convert via Readable.toWeb(nodeStream) before passing.");
738
+ }
739
+ const reader = stream.getReader.call(contents);
740
+ const chunks = [];
741
+ while (true) {
742
+ const { done, value } = await reader.read();
743
+ if (done)
744
+ break;
745
+ if (value)
746
+ chunks.push(value);
747
+ }
748
+ return Buffer5.concat(chunks.map((c) => Buffer5.from(c)));
749
+ }
266
750
  }
267
- return await Bun.write(path, data);
268
- }
269
- async function writeJsonFile(file) {
270
- let json = JSON.stringify(file.data, undefined, file.indent);
271
- if (file.newline)
272
- json += file.newline;
273
- return writeTextFile({ ...file, data: json });
274
- }
275
- function readTextFile(name, cwd) {
276
- return new Promise((resolve, reject) => {
277
- let filePath;
278
- if (cwd)
279
- filePath = join4(cwd, name);
280
- else
281
- filePath = name;
282
- fs.readFile(filePath, "utf8", (err2, text) => {
283
- if (err2) {
284
- reject(err2);
285
- } else {
286
- resolve({
287
- path: filePath,
288
- data: text
751
+ async write(path2, contents) {
752
+ const key = this.prefixPath(path2);
753
+ const body = await this.contentsToBuffer(contents);
754
+ const contentType = this.detectMimeType(path2);
755
+ await (await this.getClient()).putObject({
756
+ bucket: this.bucket,
757
+ key,
758
+ body,
759
+ contentType
760
+ });
761
+ return {
762
+ path: path2,
763
+ size: body.length,
764
+ contentType,
765
+ lastModified: Date.now()
766
+ };
767
+ }
768
+ async read(path2) {
769
+ const key = this.prefixPath(path2);
770
+ const response = await (await this.getClient()).getObject(this.bucket, key);
771
+ if (!response) {
772
+ throw new Error(`Failed to read file: ${path2}`);
773
+ }
774
+ return Buffer5.from(response);
775
+ }
776
+ async getStream(path2, _options) {
777
+ const key = this.prefixPath(path2);
778
+ const buf = await (await this.getClient()).getObjectBuffer(this.bucket, key);
779
+ if (!buf)
780
+ throw new Error(`Failed to read file: ${path2}`);
781
+ const bytes = new Uint8Array(buf);
782
+ return new ReadableStream({
783
+ start(controller) {
784
+ controller.enqueue(bytes);
785
+ controller.close();
786
+ }
787
+ });
788
+ }
789
+ async putStream(path2, stream, options) {
790
+ const key = this.prefixPath(path2);
791
+ const contentType = options?.contentType ?? this.detectMimeType(path2);
792
+ const partSize = clampPartSize(options?.partSize ?? 5242880);
793
+ const concurrency = Math.max(1, Math.min(options?.concurrency ?? 4, 100));
794
+ const maxRetries = Math.max(0, options?.maxRetries ?? 3);
795
+ const signal = options?.signal;
796
+ const reader = stream.getReader();
797
+ let firstChunk = null;
798
+ let firstDone = false;
799
+ {
800
+ const buf = new ChunkBuffer(partSize);
801
+ while (!firstDone && buf.length < partSize) {
802
+ if (signal?.aborted) {
803
+ try {
804
+ reader.releaseLock();
805
+ } catch {}
806
+ throw new Error("aborted");
807
+ }
808
+ const { value, done } = await reader.read();
809
+ if (done) {
810
+ firstDone = true;
811
+ break;
812
+ }
813
+ if (value)
814
+ buf.push(value);
815
+ }
816
+ firstChunk = buf.flush();
817
+ }
818
+ if (firstDone) {
819
+ try {
820
+ reader.releaseLock();
821
+ } catch {}
822
+ await (await this.getClient()).putObject({
823
+ bucket: this.bucket,
824
+ key,
825
+ body: Buffer5.from(firstChunk),
826
+ contentType
827
+ });
828
+ return { path: path2, size: firstChunk.length, contentType, lastModified: Date.now() };
829
+ }
830
+ const { UploadId: uploadId } = await (await this.getClient()).createMultipartUpload(this.bucket, key, { contentType });
831
+ const completedParts = [];
832
+ let totalBytes = 0;
833
+ let partNumber = 1;
834
+ const inflight = [];
835
+ const uploadOne = async (body, n) => {
836
+ let attempt = 0;
837
+ while (true) {
838
+ if (signal?.aborted)
839
+ throw new Error("aborted");
840
+ try {
841
+ const { ETag } = await (await this.getClient()).uploadPart(this.bucket, key, uploadId, n, Buffer5.from(body));
842
+ completedParts.push({ PartNumber: n, ETag });
843
+ totalBytes += body.length;
844
+ return;
845
+ } catch (err3) {
846
+ if (attempt >= maxRetries)
847
+ throw err3;
848
+ attempt += 1;
849
+ }
850
+ }
851
+ };
852
+ try {
853
+ inflight.push(uploadOne(firstChunk, partNumber++));
854
+ firstChunk = null;
855
+ const buf = new ChunkBuffer(partSize);
856
+ while (true) {
857
+ if (signal?.aborted)
858
+ throw new Error("aborted");
859
+ const { value, done } = await reader.read();
860
+ if (done)
861
+ break;
862
+ if (value)
863
+ buf.push(value);
864
+ while (buf.length >= partSize) {
865
+ const part = buf.take(partSize);
866
+ if (inflight.length >= concurrency) {
867
+ await Promise.race(inflight.map((p3, i) => p3.then(() => i)));
868
+ for (let i = inflight.length - 1;i >= 0; i--) {
869
+ if (await isSettled(inflight[i]))
870
+ inflight.splice(i, 1);
871
+ }
872
+ }
873
+ inflight.push(uploadOne(part, partNumber++));
874
+ }
875
+ }
876
+ try {
877
+ reader.releaseLock();
878
+ } catch {}
879
+ const tail = buf.flush();
880
+ if (tail.length > 0)
881
+ inflight.push(uploadOne(tail, partNumber++));
882
+ await Promise.all(inflight);
883
+ completedParts.sort((a, b) => a.PartNumber - b.PartNumber);
884
+ await (await this.getClient()).completeMultipartUpload(this.bucket, key, uploadId, completedParts);
885
+ return { path: path2, size: totalBytes, contentType, lastModified: Date.now() };
886
+ } catch (err3) {
887
+ try {
888
+ await (await this.getClient()).abortMultipartUpload(this.bucket, key, uploadId);
889
+ } catch {}
890
+ throw err3;
891
+ }
892
+ }
893
+ async readToString(path2) {
894
+ const key = this.prefixPath(path2);
895
+ const response = await (await this.getClient()).getObject(this.bucket, key);
896
+ if (!response) {
897
+ throw new Error(`Failed to read file: ${path2}`);
898
+ }
899
+ return response;
900
+ }
901
+ async readToBuffer(path2) {
902
+ const contents = await this.read(path2);
903
+ return contents;
904
+ }
905
+ async readToUint8Array(path2) {
906
+ const buffer = await this.readToBuffer(path2);
907
+ return new Uint8Array(buffer);
908
+ }
909
+ async deleteFile(path2) {
910
+ const key = this.prefixPath(path2);
911
+ await (await this.getClient()).deleteObject(this.bucket, key);
912
+ }
913
+ async deleteDirectory(path2) {
914
+ const prefix = this.prefixPath(path2);
915
+ const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
916
+ const objects = await (await this.getClient()).listAllObjects({ bucket: this.bucket, prefix: normalizedPrefix });
917
+ const keys = objects.map((obj) => obj.Key).filter((k) => typeof k === "string");
918
+ if (keys.length === 0) {
919
+ return;
920
+ }
921
+ await (await this.getClient()).deleteObjects(this.bucket, keys);
922
+ }
923
+ async createDirectory(_path) {}
924
+ async moveFile(from, to) {
925
+ await this.copyFile(from, to);
926
+ await this.deleteFile(from);
927
+ }
928
+ async copyFile(from, to) {
929
+ const fromKey = this.prefixPath(from);
930
+ const toKey = this.prefixPath(to);
931
+ await (await this.getClient()).copyObject({
932
+ sourceBucket: this.bucket,
933
+ sourceKey: fromKey,
934
+ destinationBucket: this.bucket,
935
+ destinationKey: toKey
936
+ });
937
+ }
938
+ async stat(path2) {
939
+ const key = this.prefixPath(path2);
940
+ const result = await (await this.getClient()).headObject(this.bucket, key);
941
+ if (!result) {
942
+ throw new Error(`File not found: ${path2}`);
943
+ }
944
+ return {
945
+ path: path2,
946
+ type: "file",
947
+ visibility: "private",
948
+ size: result.ContentLength || 0,
949
+ lastModified: result.LastModified ? new Date(result.LastModified).getTime() : Date.now(),
950
+ mimeType: result.ContentType
951
+ };
952
+ }
953
+ list(path2, options = {}) {
954
+ return this.createAsyncIterator(path2, options.deep || false);
955
+ }
956
+ async* createAsyncIterator(path2, deep) {
957
+ const prefix = this.prefixPath(path2);
958
+ const normalizedPrefix = prefix ? `${prefix}/` : undefined;
959
+ if (deep) {
960
+ const objects = await (await this.getClient()).listAllObjects({ bucket: this.bucket, prefix: normalizedPrefix });
961
+ for (const obj of objects) {
962
+ yield {
963
+ path: this.stripPrefix(obj.Key),
964
+ type: "file"
965
+ };
966
+ }
967
+ } else {
968
+ let continuationToken;
969
+ do {
970
+ const result = await (await this.getClient()).listObjects({
971
+ bucket: this.bucket,
972
+ prefix: normalizedPrefix,
973
+ continuationToken
289
974
  });
975
+ for (const obj of result.objects || []) {
976
+ yield {
977
+ path: this.stripPrefix(obj.Key),
978
+ type: "file"
979
+ };
980
+ }
981
+ continuationToken = result.nextContinuationToken;
982
+ } while (continuationToken);
983
+ }
984
+ }
985
+ async changeVisibility(path2, vis) {
986
+ const key = this.prefixPath(path2);
987
+ const acl = vis === "public" ? "public-read" : "private";
988
+ await (await this.getClient()).putObjectAcl(this.bucket, key, acl);
989
+ }
990
+ async visibility(path2) {
991
+ const key = this.prefixPath(path2);
992
+ const acl = await (await this.getClient()).getObjectAcl(this.bucket, key);
993
+ const grants = acl?.Grants ?? [];
994
+ const isPublic = grants.some((g) => g.Grantee?.URI === "http://acs.amazonaws.com/groups/global/AllUsers" && (g.Permission === "READ" || g.Permission === "FULL_CONTROL"));
995
+ return isPublic ? "public" : "private";
996
+ }
997
+ async fileExists(path2) {
998
+ const key = this.prefixPath(path2);
999
+ try {
1000
+ const result = await (await this.getClient()).headObject(this.bucket, key);
1001
+ return !!result;
1002
+ } catch (error) {
1003
+ if (!error.message?.includes("404") && !error.message?.includes("NoSuchKey") && !error.message?.includes("NotFound")) {
1004
+ console.debug(`[s3] Unexpected error checking file existence for ${path2}: ${error.message}`);
290
1005
  }
1006
+ return false;
1007
+ }
1008
+ }
1009
+ async directoryExists(path2) {
1010
+ const prefix = this.prefixPath(path2);
1011
+ const result = await (await this.getClient()).listObjects({
1012
+ bucket: this.bucket,
1013
+ prefix: `${prefix}/`,
1014
+ maxKeys: 1
291
1015
  });
292
- });
1016
+ return (result.objects || []).length > 0;
1017
+ }
1018
+ async publicUrl(path2, options = {}) {
1019
+ const key = this.prefixPath(path2);
1020
+ const domain = options.domain || `https://${this.bucket}.s3.${this.region}.amazonaws.com`;
1021
+ return `${domain}/${key}`;
1022
+ }
1023
+ async temporaryUrl(path2, options) {
1024
+ const key = this.prefixPath(path2);
1025
+ const expiresIn = Math.floor(normalizeExpiryToMilliseconds(options.expiresIn) / 1000);
1026
+ const MIN_EXPIRY2 = 60;
1027
+ const MAX_EXPIRY2 = 604800;
1028
+ if (!Number.isFinite(expiresIn) || expiresIn < MIN_EXPIRY2 || expiresIn > MAX_EXPIRY2) {
1029
+ throw new RangeError(`[storage/s3] temporaryUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);
1030
+ }
1031
+ return await (await this.getClient()).getSignedUrl({
1032
+ bucket: this.bucket,
1033
+ key,
1034
+ expiresIn,
1035
+ operation: "getObject"
1036
+ });
1037
+ }
1038
+ async signedUrl(path2, options) {
1039
+ return this.temporaryUrl(path2, { expiresIn: options.expiresIn });
1040
+ }
1041
+ async presignedUploadUrl(options) {
1042
+ if (!options.contentType)
1043
+ throw new Error("[storage/s3] presignedUploadUrl requires `contentType` \u2014 S3 signs against the exact header.");
1044
+ const expiresIn = Math.floor(options.expiresIn);
1045
+ const MIN_EXPIRY2 = 60;
1046
+ const MAX_EXPIRY2 = 604800;
1047
+ if (!Number.isFinite(expiresIn) || expiresIn < MIN_EXPIRY2 || expiresIn > MAX_EXPIRY2) {
1048
+ throw new RangeError(`[storage/s3] presignedUploadUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);
1049
+ }
1050
+ const safeDir = sanitizePresignedDir(options.dir);
1051
+ const safeFilename = options.filename !== undefined ? sanitizePresignedFilename(options.filename) : `${crypto.randomUUID().replace(/-/g, "")}${this.extensionForContentType(options.contentType)}`;
1052
+ const path2 = safeDir ? `${safeDir}/${safeFilename}` : safeFilename;
1053
+ const key = this.prefixPath(path2);
1054
+ const url = await (await this.getClient()).getSignedUrl({
1055
+ bucket: this.bucket,
1056
+ key,
1057
+ expiresIn,
1058
+ operation: "putObject"
1059
+ });
1060
+ return {
1061
+ url,
1062
+ path: path2,
1063
+ key,
1064
+ contentType: options.contentType,
1065
+ maxBytes: options.maxBytes
1066
+ };
1067
+ }
1068
+ async presignedUploadPolicy(options) {
1069
+ const credentials = this.resolveCredentials();
1070
+ const scopedKey = typeof options.key === "string" ? this.prefixPath(options.key) : { startsWith: this.prefixPath(options.key.startsWith) };
1071
+ return signS3PresignedPost({
1072
+ bucket: this.bucket,
1073
+ region: this.region,
1074
+ credentials,
1075
+ key: scopedKey,
1076
+ contentType: options.contentType,
1077
+ contentLengthRange: options.contentLengthRange,
1078
+ acl: options.acl,
1079
+ expiresIn: options.expiresIn,
1080
+ fields: options.fields
1081
+ });
1082
+ }
1083
+ extensionForContentType(contentType) {
1084
+ const mime = contentType.toLowerCase().split(";")[0]?.trim() ?? "";
1085
+ const map = {
1086
+ "image/jpeg": ".jpg",
1087
+ "image/jpg": ".jpg",
1088
+ "image/png": ".png",
1089
+ "image/webp": ".webp",
1090
+ "image/gif": ".gif",
1091
+ "image/avif": ".avif",
1092
+ "image/svg+xml": ".svg",
1093
+ "application/pdf": ".pdf",
1094
+ "application/json": ".json",
1095
+ "application/zip": ".zip",
1096
+ "text/plain": ".txt",
1097
+ "text/csv": ".csv",
1098
+ "video/mp4": ".mp4",
1099
+ "video/webm": ".webm",
1100
+ "audio/mpeg": ".mp3",
1101
+ "audio/wav": ".wav"
1102
+ };
1103
+ return map[mime] ?? "";
1104
+ }
1105
+ async checksum(path2, options = {}) {
1106
+ const algorithm = options.algorithm || "sha256";
1107
+ const content = await this.readToUint8Array(path2);
1108
+ const hasher = new Bun.CryptoHasher(algorithm);
1109
+ hasher.update(content);
1110
+ return hasher.digest("hex");
1111
+ }
1112
+ async mimeType(path2, _options = {}) {
1113
+ const stats = await this.stat(path2);
1114
+ return stats.mimeType || this.detectMimeType(path2);
1115
+ }
1116
+ detectMimeType(path2) {
1117
+ const ext = basename3(path2).split(".").pop()?.toLowerCase();
1118
+ const mimeTypes = {
1119
+ txt: "text/plain",
1120
+ html: "text/html",
1121
+ css: "text/css",
1122
+ js: "application/javascript",
1123
+ json: "application/json",
1124
+ xml: "application/xml",
1125
+ pdf: "application/pdf",
1126
+ zip: "application/zip",
1127
+ jpg: "image/jpeg",
1128
+ jpeg: "image/jpeg",
1129
+ png: "image/png",
1130
+ gif: "image/gif",
1131
+ svg: "image/svg+xml",
1132
+ mp4: "video/mp4",
1133
+ mp3: "audio/mpeg",
1134
+ wav: "audio/wav"
1135
+ };
1136
+ return mimeTypes[ext || ""] || "application/octet-stream";
1137
+ }
1138
+ async lastModified(path2) {
1139
+ const stats = await this.stat(path2);
1140
+ return stats.lastModified;
1141
+ }
1142
+ async fileSize(path2) {
1143
+ const stats = await this.stat(path2);
1144
+ return stats.size;
1145
+ }
293
1146
  }
294
- async function writeTextFile(file) {
295
- return await Bun.write(file.path, file.data);
1147
+ function createS3Storage(client, config) {
1148
+ return new S3StorageAdapter(client, config);
296
1149
  }
297
- function isFile(path) {
298
- return fs.existsSync(path);
1150
+ var S3_MIN_PART_SIZE, S3_MAX_PART_SIZE;
1151
+ var init_s3 = __esm(() => {
1152
+ init_types();
1153
+ init_path_sanitize();
1154
+ init_s3_presigned_post();
1155
+ S3_MIN_PART_SIZE = 5 * 1024 * 1024;
1156
+ S3_MAX_PART_SIZE = 5 * 1024 * 1024 * 1024;
1157
+ });
1158
+
1159
+ // src/put-file.ts
1160
+ function extFromOriginalName(name) {
1161
+ if (!name)
1162
+ return null;
1163
+ const idx = name.lastIndexOf(".");
1164
+ if (idx <= 0 || idx === name.length - 1)
1165
+ return null;
1166
+ const ext = name.slice(idx + 1).toLowerCase();
1167
+ if (!/^[a-z0-9]+$/.test(ext))
1168
+ return null;
1169
+ return ext;
1170
+ }
1171
+ function originalNameOf(file2) {
1172
+ return file2.originalName ?? file2.name;
1173
+ }
1174
+ function mimetypeOf(file2) {
1175
+ return file2.mimetype ?? file2.mimeType;
1176
+ }
1177
+ function deriveExtension(file2) {
1178
+ const mime = mimetypeOf(file2);
1179
+ return extFromOriginalName(originalNameOf(file2)) ?? (mime && MIME_TO_EXT[mime.toLowerCase()]) ?? null;
1180
+ }
1181
+ async function readBytes(file2) {
1182
+ if (file2.buffer !== undefined)
1183
+ return file2.buffer;
1184
+ if (typeof file2.bytes === "function")
1185
+ return await file2.bytes();
1186
+ if (typeof file2.arrayBuffer === "function")
1187
+ return await file2.arrayBuffer();
1188
+ throw new Error("UploadedFile is missing both `buffer` and `bytes()`/`arrayBuffer()` accessors \u2014 cannot read file contents.");
1189
+ }
1190
+ function bufferLikeToHash(buffer) {
1191
+ const view = buffer instanceof ArrayBuffer ? new Uint8Array(buffer) : buffer;
1192
+ const hasher = new Bun.CryptoHasher("sha256");
1193
+ hasher.update(view);
1194
+ return hasher.digest("hex").slice(0, 32);
1195
+ }
1196
+ function sanitizeOriginalName(name) {
1197
+ const stripped = name.replace(/[/\\]/g, "_").replace(/\.{2,}/g, "_");
1198
+ return stripped.replace(/[^A-Za-z0-9._-]/g, "_").replace(/_+/g, "_");
1199
+ }
1200
+ async function resolveFilename(file2, strategy) {
1201
+ if (typeof strategy === "function")
1202
+ return strategy(file2);
1203
+ switch (strategy) {
1204
+ case "uuid":
1205
+ return crypto.randomUUID().replace(/-/g, "");
1206
+ case "hash": {
1207
+ const bytes = await readBytes(file2);
1208
+ return bufferLikeToHash(bytes);
1209
+ }
1210
+ case "original": {
1211
+ const name = originalNameOf(file2);
1212
+ return name ? sanitizeOriginalName(name) : crypto.randomUUID().replace(/-/g, "");
1213
+ }
1214
+ }
1215
+ }
1216
+ function joinPath(...parts) {
1217
+ return parts.filter(Boolean).map((p3, i) => i === 0 ? p3.replace(/\/+$/, "") : p3.replace(/^\/+/, "").replace(/\/+$/, "")).filter(Boolean).join("/");
1218
+ }
1219
+ async function putUploadedFile(manager, file2, opts) {
1220
+ const disk = manager.disk(opts.disk);
1221
+ const baseName = await resolveFilename(file2, opts.filename ?? "uuid");
1222
+ const wantExt = opts.preserveExtension !== false;
1223
+ const baseHasExt = /\.[A-Za-z0-9]+$/.test(baseName);
1224
+ const ext = wantExt && !baseHasExt ? deriveExtension(file2) : null;
1225
+ const finalName = ext ? `${baseName}.${ext}` : baseName;
1226
+ const fullPath = joinPath(opts.dir ?? "", finalName);
1227
+ const raw = await readBytes(file2);
1228
+ let contents = raw instanceof ArrayBuffer ? new Uint8Array(raw) : raw;
1229
+ if (opts.transform) {
1230
+ contents = await opts.transform(contents);
1231
+ }
1232
+ const written = await disk.write(fullPath, contents);
1233
+ const url = await disk.publicUrl(fullPath);
1234
+ return { ...written, path: fullPath, url };
1235
+ }
1236
+ var MIME_TO_EXT;
1237
+ var init_put_file = __esm(() => {
1238
+ MIME_TO_EXT = {
1239
+ "image/jpeg": "jpg",
1240
+ "image/jpg": "jpg",
1241
+ "image/png": "png",
1242
+ "image/webp": "webp",
1243
+ "image/gif": "gif",
1244
+ "image/svg+xml": "svg",
1245
+ "image/avif": "avif",
1246
+ "application/pdf": "pdf",
1247
+ "application/json": "json",
1248
+ "application/zip": "zip",
1249
+ "application/octet-stream": "bin",
1250
+ "text/plain": "txt",
1251
+ "text/csv": "csv",
1252
+ "text/html": "html",
1253
+ "video/mp4": "mp4",
1254
+ "video/webm": "webm",
1255
+ "audio/mpeg": "mp3",
1256
+ "audio/wav": "wav"
1257
+ };
1258
+ });
1259
+
1260
+ // src/facade.ts
1261
+ var exports_facade = {};
1262
+ __export(exports_facade, {
1263
+ StorageManager: () => StorageManager,
1264
+ Storage: () => Storage
1265
+ });
1266
+ import { resolve as resolve3 } from "path";
1267
+ import process6 from "process";
1268
+ import { filesystems, app as appConfig } from "@stacksjs/config";
1269
+ function buildConfig() {
1270
+ const cwd = process6.cwd();
1271
+ const rootDir = filesystems.root || cwd;
1272
+ const s3Config = filesystems.s3;
1273
+ const appUrl = appConfig?.url || "";
1274
+ const config = {
1275
+ default: filesystems.driver || "local",
1276
+ disks: {
1277
+ local: {
1278
+ driver: "local",
1279
+ root: resolve3(rootDir, "storage/app"),
1280
+ visibility: filesystems.defaultVisibility || "private"
1281
+ },
1282
+ public: {
1283
+ driver: "local",
1284
+ root: resolve3(rootDir, "public"),
1285
+ url: appUrl ? `${appUrl}/storage` : "/storage",
1286
+ visibility: "public"
1287
+ }
1288
+ }
1289
+ };
1290
+ if (s3Config?.bucket) {
1291
+ config.disks.s3 = {
1292
+ driver: "s3",
1293
+ bucket: s3Config.bucket,
1294
+ region: s3Config.region || "us-east-1",
1295
+ prefix: s3Config.prefix,
1296
+ endpoint: s3Config.endpoint,
1297
+ url: filesystems.publicUrl?.domain,
1298
+ usePathStyleEndpoint: !!s3Config.endpoint,
1299
+ visibility: filesystems.defaultVisibility || "private",
1300
+ credentials: s3Config.credentials ? { key: s3Config.credentials.accessKeyId, secret: s3Config.credentials.secretAccessKey } : undefined
1301
+ };
1302
+ }
1303
+ return config;
299
1304
  }
300
- function doesExist(path) {
301
- return isFile(path) || isFolder(path);
1305
+
1306
+ class StorageManager {
1307
+ _config = null;
1308
+ disks = new Map;
1309
+ customConfig = null;
1310
+ get config() {
1311
+ if (!this._config) {
1312
+ const builtConfig = buildConfig();
1313
+ this._config = this.customConfig ? {
1314
+ default: this.customConfig.default || builtConfig.default,
1315
+ disks: { ...builtConfig.disks, ...this.customConfig.disks }
1316
+ } : builtConfig;
1317
+ }
1318
+ return this._config;
1319
+ }
1320
+ init(config) {
1321
+ this.customConfig = config;
1322
+ this._config = null;
1323
+ this.disks.clear();
1324
+ return this;
1325
+ }
1326
+ disk(name) {
1327
+ const diskName = name || this.config.default;
1328
+ if (this.disks.has(diskName)) {
1329
+ return this.disks.get(diskName);
1330
+ }
1331
+ const diskConfig = this.config.disks[diskName];
1332
+ if (!diskConfig) {
1333
+ const available = Object.keys(this.config.disks).join(", ");
1334
+ throw new Error(`Disk [${diskName}] is not configured. Available: ${available}`);
1335
+ }
1336
+ const adapter2 = this.createAdapter(diskName, diskConfig);
1337
+ this.disks.set(diskName, adapter2);
1338
+ return adapter2;
1339
+ }
1340
+ createAdapter(name, config) {
1341
+ switch (config.driver) {
1342
+ case "local":
1343
+ return this.createLocalAdapter(config);
1344
+ case "s3":
1345
+ return this.createS3Adapter(name, config);
1346
+ default:
1347
+ throw new Error(`Unsupported driver: ${config.driver}`);
1348
+ }
1349
+ }
1350
+ createLocalAdapter(config) {
1351
+ return createLocalStorage({ root: config.root });
1352
+ }
1353
+ createS3Adapter(_name, config) {
1354
+ return new S3StorageAdapter(null, {
1355
+ bucket: config.bucket,
1356
+ region: config.region,
1357
+ prefix: config.prefix
1358
+ });
1359
+ }
1360
+ async put(pathOrFile, contentsOrOpts) {
1361
+ if (typeof pathOrFile === "string") {
1362
+ return this.disk().write(pathOrFile, contentsOrOpts);
1363
+ }
1364
+ const opts = contentsOrOpts ?? {};
1365
+ return putUploadedFile(this, pathOrFile, opts);
1366
+ }
1367
+ async stat(path2) {
1368
+ return this.disk().stat(path2);
1369
+ }
1370
+ async getStream(path2, options) {
1371
+ const adapter2 = this.disk();
1372
+ if (typeof adapter2.getStream !== "function") {
1373
+ throw new Error(`[storage] disk '${this.config.default}' does not support getStream \u2014 adapter is missing the optional method`);
1374
+ }
1375
+ return adapter2.getStream(path2, options);
1376
+ }
1377
+ async putStream(path2, stream, options) {
1378
+ const adapter2 = this.disk();
1379
+ if (typeof adapter2.putStream !== "function") {
1380
+ throw new Error(`[storage] disk '${this.config.default}' does not support putStream \u2014 adapter is missing the optional method`);
1381
+ }
1382
+ return adapter2.putStream(path2, stream, options);
1383
+ }
1384
+ async copyAcross(source, dest) {
1385
+ const src = parseDiskPath(source);
1386
+ const dst = parseDiskPath(dest);
1387
+ if (src.disk === dst.disk) {
1388
+ const adapter2 = this.disk(src.disk);
1389
+ await adapter2.copyFile(src.path, dst.path);
1390
+ return adapter2.stat(dst.path).then((entry) => ({
1391
+ path: dst.path,
1392
+ size: entry.size,
1393
+ contentType: entry.mimeType,
1394
+ lastModified: entry.lastModified
1395
+ }));
1396
+ }
1397
+ const contents = await this.disk(src.disk).read(src.path);
1398
+ return this.disk(dst.disk).write(dst.path, contents);
1399
+ }
1400
+ async moveAcross(source, dest) {
1401
+ const src = parseDiskPath(source);
1402
+ const result = await this.copyAcross(source, dest);
1403
+ await this.disk(src.disk).deleteFile(src.path);
1404
+ return result;
1405
+ }
1406
+ async get(path2) {
1407
+ return this.disk().readToString(path2);
1408
+ }
1409
+ async exists(path2) {
1410
+ return this.disk().fileExists(path2);
1411
+ }
1412
+ async missing(path2) {
1413
+ return !await this.exists(path2);
1414
+ }
1415
+ async delete(path2) {
1416
+ return this.disk().deleteFile(path2);
1417
+ }
1418
+ async copy(from, to) {
1419
+ return this.disk().copyFile(from, to);
1420
+ }
1421
+ async move(from, to) {
1422
+ return this.disk().moveFile(from, to);
1423
+ }
1424
+ async url(path2) {
1425
+ return this.disk().publicUrl(path2);
1426
+ }
1427
+ async signedUrl(path2, options) {
1428
+ const adapter2 = this.disk();
1429
+ if (typeof adapter2.signedUrl !== "function") {
1430
+ throw new Error(`[storage] disk '${this.config.default}' does not support signedUrl`);
1431
+ }
1432
+ return adapter2.signedUrl(path2, options);
1433
+ }
1434
+ async presignedUploadUrl(options) {
1435
+ const adapter2 = this.disk();
1436
+ if (typeof adapter2.presignedUploadUrl !== "function") {
1437
+ throw new Error(`[storage] disk '${this.config.default}' does not support presignedUploadUrl \u2014 only S3-style adapters do. Use \`Storage.put(file, opts)\` for local/proxied uploads.`);
1438
+ }
1439
+ return adapter2.presignedUploadUrl(options);
1440
+ }
1441
+ async presignedUploadPolicy(options) {
1442
+ const adapter2 = this.disk();
1443
+ if (typeof adapter2.presignedUploadPolicy !== "function") {
1444
+ throw new Error(`[storage] disk '${this.config.default}' does not support presignedUploadPolicy \u2014 S3-only. Use \`presignedUploadUrl\` for the PUT-form, or \`Storage.put(file, opts)\` for server-proxied uploads.`);
1445
+ }
1446
+ return adapter2.presignedUploadPolicy(options);
1447
+ }
1448
+ async size(path2) {
1449
+ return this.disk().fileSize(path2);
1450
+ }
1451
+ async lastModified(path2) {
1452
+ return this.disk().lastModified(path2);
1453
+ }
1454
+ async mimeType(path2) {
1455
+ return this.disk().mimeType(path2);
1456
+ }
1457
+ async checksum(path2, algorithm) {
1458
+ return this.disk().checksum(path2, { algorithm });
1459
+ }
1460
+ async makeDirectory(path2) {
1461
+ return this.disk().createDirectory(path2);
1462
+ }
1463
+ async deleteDirectory(path2) {
1464
+ return this.disk().deleteDirectory(path2);
1465
+ }
1466
+ files(path2 = "") {
1467
+ return this.disk().list(path2);
1468
+ }
1469
+ allFiles(path2 = "") {
1470
+ return this.disk().list(path2, { deep: true });
1471
+ }
1472
+ configure(name, config) {
1473
+ const currentConfig = this.config;
1474
+ currentConfig.disks[name] = config;
1475
+ this.disks.delete(name);
1476
+ return this;
1477
+ }
1478
+ setDefaultDisk(name) {
1479
+ if (!this.config.disks[name]) {
1480
+ throw new Error(`Disk [${name}] is not configured`);
1481
+ }
1482
+ this.config.default = name;
1483
+ return this;
1484
+ }
1485
+ getDiskConfig(name) {
1486
+ return this.config.disks[name || this.config.default];
1487
+ }
1488
+ getConfiguredDisks() {
1489
+ return Object.keys(this.config.disks);
1490
+ }
1491
+ getDefaultDisk() {
1492
+ return this.config.default;
1493
+ }
1494
+ reset() {
1495
+ this._config = null;
1496
+ this.customConfig = null;
1497
+ this.disks.clear();
1498
+ return this;
1499
+ }
302
1500
  }
303
- function doesNotExist(path) {
304
- return !isFile(path) && !isFolder(path);
1501
+ var Storage;
1502
+ var init_facade = __esm(() => {
1503
+ init_local();
1504
+ init_s3();
1505
+ init_path_sanitize();
1506
+ init_put_file();
1507
+ Storage = new StorageManager;
1508
+ });
1509
+
1510
+ // src/copy.ts
1511
+ import { contains } from "@stacksjs/arrays";
1512
+ import { join } from "@stacksjs/path";
1513
+
1514
+ // src/fs.ts
1515
+ import * as fs from "fs";
1516
+ import { existsSync, watch as fsWatch, mkdirSync, readFileSync, watchFile, writeFileSync } from "fs";
1517
+ function exists(path) {
1518
+ return existsSync(path);
305
1519
  }
306
- function hasFiles(folder) {
307
- try {
308
- return fs.readdirSync(folder).length > 0;
309
- } catch (err2) {
310
- log2.debug(`Error reading folder: ${folder}`, err2);
311
- return false;
1520
+
1521
+ // src/copy.ts
1522
+ function copy(src, dest, exclude = []) {
1523
+ if (Array.isArray(src)) {
1524
+ src.forEach((file) => {
1525
+ copy(file, dest, exclude);
1526
+ });
1527
+ } else {
1528
+ if (fs.statSync(src).isDirectory())
1529
+ copyFolder(src, dest, exclude);
1530
+ else
1531
+ copyFile(src, dest);
312
1532
  }
313
1533
  }
314
- function hasComponents() {
315
- return hasFiles(p.componentsPath());
316
- }
317
- function hasFunctions() {
318
- return hasFiles(p.functionsPath());
1534
+ function copyFile(src, dest) {
1535
+ fs.copyFileSync(src, dest);
319
1536
  }
320
- function deleteFiles(dir, exclude = []) {
321
- if (fs.existsSync(dir)) {
322
- fs.readdirSync(dir).forEach((file) => {
323
- const p2 = join4(dir, file);
324
- if (fs.statSync(p2).isDirectory()) {
325
- if (fs.readdirSync(p2).length === 0)
326
- fs.rmSync(p2, { recursive: true, force: true });
1537
+ function copyFolder(src, dest, exclude = []) {
1538
+ if (!fs.existsSync(dest))
1539
+ fs.mkdirSync(dest, { recursive: true });
1540
+ if (fs.existsSync(src)) {
1541
+ fs.readdirSync(src).forEach((file) => {
1542
+ if (!contains(join(src, file), exclude)) {
1543
+ const srcPath = join(src, file);
1544
+ const destPath = join(dest, file);
1545
+ if (fs.statSync(srcPath).isDirectory())
1546
+ copyFolder(srcPath, destPath, exclude);
327
1547
  else
328
- deleteFiles(p2, exclude);
329
- } else if (!contains2(p2, exclude)) {
330
- fs.rmSync(p2);
1548
+ fs.copyFileSync(srcPath, destPath);
331
1549
  }
332
1550
  });
333
1551
  }
334
1552
  }
335
- function getFiles(dir, exclude = []) {
336
- let results = [];
337
- const list = fs.readdirSync(dir);
338
- list.forEach((file) => {
339
- file = join4(dir, file);
340
- const stat = fs.statSync(file);
341
- if (stat.isDirectory())
342
- results = results.concat(getFiles(file, exclude));
343
- else if (!contains2(file, exclude))
344
- results.push(file);
345
- });
346
- return results;
1553
+ // src/delete.ts
1554
+ import { italic, log } from "@stacksjs/cli";
1555
+ import { err, handleError, ok } from "@stacksjs/error-handling";
1556
+ import { join as join3 } from "@stacksjs/path";
1557
+
1558
+ // src/folders.ts
1559
+ import { join as join2 } from "@stacksjs/path";
1560
+ function isFolder(path) {
1561
+ try {
1562
+ return fs.statSync(path).isDirectory();
1563
+ } catch {
1564
+ return false;
1565
+ }
347
1566
  }
348
- function put(path, contents) {
349
- const dirPath = dirname(path);
350
- if (!fs.existsSync(dirPath))
351
- fs.mkdirSync(dirPath, { recursive: true });
352
- fs.writeFileSync(path, contents, "utf-8");
1567
+ function isDir(path) {
1568
+ return isFolder(path);
353
1569
  }
354
- async function get(path) {
355
- return Bun.file(path).text();
1570
+ function doesFolderExist(path) {
1571
+ return fs.existsSync(path);
356
1572
  }
357
- var files = {
358
- readJsonFile,
359
- readPackageJson,
360
- readTextFile,
361
- writeJsonFile,
362
- writeTextFile,
363
- hasFiles,
364
- hasComponents,
365
- hasFunctions,
366
- deleteFiles,
367
- getFiles,
368
- put,
369
- get
370
- };
371
- // src/hash.ts
372
- import { createHash } from "crypto";
373
- import { path as p2 } from "@stacksjs/path";
374
- function hashFileOrDirectory(path, hash) {
375
- if (!fs.existsSync(path)) {
376
- console.error(`Path does not exist: ${path}`);
377
- return;
378
- }
379
- if (fs.statSync(path).isDirectory()) {
380
- const files2 = fs.readdirSync(path);
381
- for (const file of files2) {
382
- const filePath = p2.join(path, file);
383
- hashFileOrDirectory(filePath, hash);
1573
+ function createFolder(dir) {
1574
+ return new Promise((resolve, reject) => {
1575
+ try {
1576
+ fs.mkdirSync(dir, { recursive: true });
1577
+ resolve();
1578
+ } catch (err) {
1579
+ reject(err);
384
1580
  }
385
- } else {
386
- hash.update(fs.readFileSync(path));
387
- }
1581
+ });
388
1582
  }
389
- function hashDirectory(directory) {
390
- const hash = createHash("sha256");
391
- hashFileOrDirectory(directory, hash);
392
- return hash.digest("hex");
1583
+ function getFolders(dir) {
1584
+ return fs.readdirSync(dir).filter((file) => {
1585
+ return fs.statSync(join2(dir, file)).isDirectory();
1586
+ });
393
1587
  }
394
- function hashPath(path) {
395
- const hash = createHash("sha256");
396
- hashFileOrDirectory(path, hash);
397
- return hash.digest("hex");
1588
+ var folders = {
1589
+ isFolder,
1590
+ doesFolderExist,
1591
+ createFolder,
1592
+ getFolders
1593
+ };
1594
+
1595
+ // src/glob.ts
1596
+ var {Glob: BunGlob } = globalThis.Bun;
1597
+ function isEnoent(err) {
1598
+ return !!err && typeof err === "object" && err.code === "ENOENT";
398
1599
  }
399
- function hashPaths(paths) {
400
- const hash = createHash("sha256");
401
- const pathsArray = Array.isArray(paths) ? paths : [paths];
402
- for (const path of pathsArray)
403
- hashFileOrDirectory(path, hash);
404
- return hash.digest("hex");
1600
+ function globSync(patterns, options) {
1601
+ const patternArray = typeof patterns === "string" ? [patterns] : patterns;
1602
+ const results = [];
1603
+ for (const pattern of patternArray) {
1604
+ try {
1605
+ const globInstance = new BunGlob(pattern);
1606
+ const matches = globInstance.scanSync({
1607
+ cwd: options?.cwd,
1608
+ absolute: options?.absolute,
1609
+ dot: options?.dot,
1610
+ onlyFiles: options?.onlyFiles
1611
+ });
1612
+ for (const match of matches) {
1613
+ results.push(match);
1614
+ }
1615
+ } catch (err) {
1616
+ if (!isEnoent(err))
1617
+ throw err;
1618
+ }
1619
+ }
1620
+ return results;
405
1621
  }
406
- // src/helpers.ts
407
- import { fileURLToPath } from "url";
408
- import { dirname as dirname2 } from "@stacksjs/path";
409
- var __dirname = "/home/runner/work/stacks/stacks/storage/framework/core/storage/src";
410
- var _dirname = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath(import.meta.url));
411
- function updateConfigFile(filePath, newConfig) {
1622
+ async function glob(patterns, options) {
1623
+ const patternArray = typeof patterns === "string" ? [patterns] : patterns;
1624
+ const results = [];
1625
+ for (const pattern of patternArray) {
1626
+ try {
1627
+ const globInstance = new BunGlob(pattern);
1628
+ const matches = globInstance.scan({
1629
+ cwd: options?.cwd,
1630
+ absolute: options?.absolute,
1631
+ dot: options?.dot,
1632
+ onlyFiles: options?.onlyFiles
1633
+ });
1634
+ for await (const match of matches) {
1635
+ results.push(match);
1636
+ }
1637
+ } catch (err) {
1638
+ if (!isEnoent(err))
1639
+ throw err;
1640
+ }
1641
+ }
1642
+ return results;
1643
+ }
1644
+
1645
+ // src/delete.ts
1646
+ function deleteFolder(path) {
412
1647
  return new Promise((resolve, reject) => {
413
- let config;
414
1648
  try {
415
- config = JSON.parse(fs.readFileSync(filePath, "utf8"));
1649
+ if (isFolder(path)) {
1650
+ fs.rmSync(path, { recursive: true, force: true });
1651
+ return resolve(ok(`Deleted ${path}`));
1652
+ }
1653
+ return resolve(ok(`Path ${path} was not a directory`));
416
1654
  } catch (error) {
417
- reject(new Error(`Failed to parse config file "${filePath}": ${error.message}`));
418
- return;
1655
+ return reject(err(error));
419
1656
  }
420
- for (const key in newConfig)
421
- config[key] = newConfig[key];
1657
+ });
1658
+ }
1659
+ async function isDirectoryEmpty(path) {
1660
+ return new Promise((resolve, reject) => {
422
1661
  try {
423
- fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
424
- resolve();
1662
+ if (fs.statSync(path).isDirectory()) {
1663
+ if (fs.readdirSync(path).length === 0)
1664
+ return resolve(ok(true));
1665
+ return resolve(ok(false));
1666
+ }
1667
+ return resolve(ok(false));
425
1668
  } catch (error) {
426
- reject(error);
1669
+ return reject(err(error));
427
1670
  }
428
1671
  });
429
1672
  }
430
- var helpers = {
431
- _dirname,
432
- updateConfigFile
433
- };
434
- // src/storage.ts
435
- var exports_storage = {};
436
- __export(exports_storage, {
437
- zip: () => zip,
438
- writeTextFile: () => writeTextFile,
439
- writeJsonFile: () => writeJsonFile,
440
- writeFileSync: () => writeFileSync,
441
- writeFile: () => writeFile,
442
- watchFile: () => watchFile,
443
- updateConfigFile: () => updateConfigFile,
444
- unzip: () => unzip,
445
- unarchive: () => unarchive,
446
- setVisibility: () => setVisibility,
447
- rename: () => rename,
448
- readTextFile: () => readTextFile,
449
- readPackageJson: () => readPackageJson,
450
- readJsonFile: () => readJsonFile,
451
- readFileSync: () => readFileSync,
452
- put: () => put,
453
- move: () => move,
454
- mkdirSync: () => mkdirSync,
455
- isFolder: () => isFolder,
456
- isDirectoryEmpty: () => isDirectoryEmpty,
457
- isDir: () => isDir,
458
- inflateSync: () => inflateSync,
459
- helpers: () => helpers,
460
- hasFunctions: () => hasFunctions,
461
- hasFiles: () => hasFiles,
462
- hasComponents: () => hasComponents,
463
- gzipSync: () => gzipSync,
464
- gunzipSync: () => gunzipSync,
465
- getFolders: () => getFolders,
466
- getFiles: () => getFiles,
467
- get: () => get,
468
- fsWatch: () => fsWatch,
469
- fs: () => fs,
470
- folders: () => folders,
471
- files: () => files,
472
- existsSync: () => existsSync,
473
- exists: () => exists,
474
- doesNotExist: () => doesNotExist,
475
- doesFolderExist: () => doesFolderExist,
476
- doesExist: () => doesExist,
477
- deleteGlob: () => deleteGlob,
478
- deleteFolder: () => deleteFolder,
479
- deleteFiles: () => deleteFiles,
480
- deleteFile: () => deleteFile,
481
- deleteEmptyFolders: () => deleteEmptyFolders,
482
- deleteEmptyFolder: () => deleteEmptyFolder,
483
- del: () => del,
484
- deflateSync: () => deflateSync,
485
- decompress: () => decompress,
486
- createFolder: () => createFolder,
487
- copyFolder: () => copyFolder,
488
- copyFile: () => copyFile,
489
- copy: () => copy,
490
- compress: () => compress,
491
- archive: () => archive,
492
- _dirname: () => _dirname
493
- });
494
-
495
- // src/move.ts
496
- import { err as err2, handleError as handleError2, ok as ok2 } from "@stacksjs/error-handling";
497
- import { log as log3 } from "@stacksjs/logging";
498
- import { path } from "@stacksjs/path";
499
- async function move(src, dest, options) {
500
- try {
501
- if (Array.isArray(src)) {
502
- const errors = [];
503
- const operations = src.map(async (file) => {
504
- const from2 = file;
505
- const to2 = path.resolve(dest, path.basename(file));
506
- const result2 = await rename(from2, to2, options);
507
- if (result2.isErr) {
508
- log3.error(result2.error);
509
- errors.push(result2.error);
510
- }
511
- });
512
- await Promise.all(operations);
513
- if (errors.length > 0)
514
- return err2(handleError2(errors[0]));
515
- return ok2({ message: "Files moved successfully" });
1673
+ async function deleteEmptyFolder(path) {
1674
+ return new Promise((resolve, reject) => {
1675
+ try {
1676
+ if (fs.statSync(path).isDirectory()) {
1677
+ if (fs.readdirSync(path).length === 0) {
1678
+ fs.rmSync(path, { recursive: true, force: true });
1679
+ return resolve(ok(`Deleted ${path}`));
1680
+ }
1681
+ return resolve(ok(`Path ${path} was not empty`));
1682
+ }
1683
+ return resolve(ok(`Path ${path} was not a directory`));
1684
+ } catch (error) {
1685
+ return reject(err(error));
516
1686
  }
517
- const from = src;
518
- const to = dest;
519
- const result = await rename(from, to, options);
520
- if (result.isErr) {
521
- log3.error(result.error);
522
- return err2(handleError2(result.error));
1687
+ });
1688
+ }
1689
+ async function deleteEmptyFolders(dir) {
1690
+ try {
1691
+ if (!fs.existsSync(dir))
1692
+ return ok(`Path ${dir} does not exist`);
1693
+ const files = fs.readdirSync(dir);
1694
+ for (const file of files) {
1695
+ const p = join3(dir, file);
1696
+ if (isFolder(p)) {
1697
+ if (fs.readdirSync(p).length === 0)
1698
+ fs.rmSync(p, { recursive: true, force: true });
1699
+ else
1700
+ await deleteEmptyFolders(p);
1701
+ }
523
1702
  }
524
- return ok2({ message: "File moved successfully" });
1703
+ return ok(`Deleted empty folders located in ${dir}`);
525
1704
  } catch (error) {
526
- return err2(handleError2(error));
1705
+ return err(error);
527
1706
  }
528
1707
  }
529
- async function rename(from, to, options) {
1708
+ function deleteFile(path) {
530
1709
  return new Promise((resolve, reject) => {
531
1710
  try {
532
- const dir = path.dirname(to);
533
- if (!fs.existsSync(dir))
534
- fs.mkdirSync(dir, { recursive: true });
535
- if (!fs.existsSync(from))
536
- return reject(err2(new Error(`File or directory does not exist: ${from}`)));
537
- if (fs.existsSync(to)) {
538
- if (!options?.overwrite)
539
- return reject(err2(new Error(`File or directory already exists: ${to}`)));
540
- fs.rmSync(to, { recursive: true, force: true });
1711
+ if (fs.statSync(path).isFile()) {
1712
+ fs.rmSync(path, { recursive: true, force: true });
1713
+ return resolve(ok(`Deleted ${path}`));
541
1714
  }
542
- fs.renameSync(from, to);
543
- return resolve(ok2({ message: "File moved successfully" }));
1715
+ return resolve(ok(`Path ${path} was not a file`));
544
1716
  } catch (error) {
545
- if (error.code === "ENOENT")
546
- log3.error(`File or directory does not exist
547
-
548
- `, error);
549
- else
550
- log3.error(error);
551
- return reject(err2(new Error(error)));
1717
+ return reject(err(error));
552
1718
  }
553
1719
  });
554
1720
  }
555
- // src/visibility.ts
556
- function setVisibility() {
557
- return "wip";
558
- }
559
- // src/zip.ts
560
- import { runCommand } from "@stacksjs/cli";
561
- function shellEscape(_arg) {
562
- return `'${_arg.replace(/'/g, "'\\''")}'`;
563
- }
564
- async function zip(from, to, options) {
565
- const toPath = to || "archive.zip";
566
- if (Array.isArray(from)) {
567
- const fromPath = from.map((f) => shellEscape(f)).join(" ");
568
- return runCommand(`zip -r ${shellEscape(toPath)} ${fromPath}`, options);
569
- }
570
- return runCommand(`zip -r ${shellEscape(toPath)} ${shellEscape(from)}`, options);
571
- }
572
- async function unzip(paths) {
573
- if (Array.isArray(paths))
574
- return runCommand(`unzip ${paths.map((p3) => shellEscape(p3)).join(" ")}`);
575
- return runCommand(`unzip ${shellEscape(paths)}`);
576
- }
577
- function archive(paths) {
578
- return zip(paths);
579
- }
580
- function unarchive(paths) {
581
- return unzip(paths);
582
- }
583
- function compress(paths) {
584
- return zip(paths);
585
- }
586
- function decompress(paths) {
587
- return unzip(paths);
588
- }
589
- function gzipSync(data, options) {
590
- return Bun.gzipSync(data, options);
591
- }
592
- function gunzipSync(data) {
593
- return Bun.gunzipSync(data);
594
- }
595
- function deflateSync(data, options) {
596
- return Bun.deflateSync(data, options);
597
- }
598
- function inflateSync(data) {
599
- return Bun.inflateSync(data);
600
- }
601
- // src/adapters/local.ts
602
- import { Buffer as Buffer2 } from "buffer";
603
- import { createHmac as createHmac2 } from "crypto";
604
- import { createWriteStream } from "fs";
605
- import { access, constants, copyFile as copyFile2, lstat, mkdir, readdir, readFile, rename as rename2, rm, unlink, writeFile as writeFile2 } from "fs/promises";
606
- import { basename, dirname as dirname3, join as join5, relative } from "path";
607
- import { pipeline } from "stream/promises";
608
-
609
- // src/types.ts
610
- var Visibility;
611
- ((Visibility2) => {
612
- Visibility2["PUBLIC"] = "public";
613
- Visibility2["PRIVATE"] = "private";
614
- })(Visibility ||= {});
615
- async function* createDirectoryListing(entries) {
616
- for (const entry of entries) {
617
- yield entry;
1721
+ async function deleteGlob(path) {
1722
+ if (!path.includes("*"))
1723
+ return err(handleError(`Path ${path} does not contain a glob`));
1724
+ const directories = await glob([path], { onlyDirectories: true });
1725
+ for (const directory of directories) {
1726
+ const result = await deleteFolder(directory);
1727
+ if (result.isErr) {
1728
+ log.error(result.error);
1729
+ return result;
1730
+ }
1731
+ log.info(`Deleted ${italic(directory)}`);
618
1732
  }
1733
+ return ok(`Deleted ${directories.length} directories`);
619
1734
  }
620
- function normalizeExpiryToMilliseconds(expiry) {
621
- if (expiry instanceof Date) {
622
- return expiry.getTime() - Date.now();
623
- }
624
- return expiry * 1000;
1735
+ async function del(path) {
1736
+ if (fs.existsSync(path) && fs.statSync(path).isFile())
1737
+ return await deleteFile(path);
1738
+ if (isFolder(path))
1739
+ return await deleteFolder(path);
1740
+ if (path.includes("*"))
1741
+ return await deleteGlob(path);
1742
+ return err(handleError(`Path ${path} cannot be deleted due to an unhandled condition. Please report this issue.`));
625
1743
  }
626
- function normalizeExpiryToDate(expiry) {
627
- if (expiry instanceof Date) {
628
- return expiry;
1744
+ // src/files.ts
1745
+ import { contains as contains2 } from "@stacksjs/arrays";
1746
+ import { log as log2 } from "@stacksjs/logging";
1747
+ import { dirname, join as join4, path as p } from "@stacksjs/path";
1748
+ import { detectIndent, detectNewline } from "@stacksjs/strings";
1749
+ async function readJsonFile(name, cwd) {
1750
+ const file = await readTextFile(name, cwd);
1751
+ let data;
1752
+ try {
1753
+ data = JSON.parse(file.data);
1754
+ } catch (error) {
1755
+ throw new Error(`Failed to parse JSON file "${name}": ${error.message}`);
629
1756
  }
630
- return new Date(Date.now() + expiry * 1000);
631
- }
632
- function isFile2(entry) {
633
- return entry.type === "file";
1757
+ const indent = detectIndent(file.data).indent;
1758
+ const newline = detectNewline(file.data);
1759
+ return { ...file, data, indent, newline };
634
1760
  }
635
- function isDirectory(entry) {
636
- return entry.type === "directory";
1761
+ async function readPackageJson(name, cwd) {
1762
+ const file = await readJsonFile(name, cwd);
1763
+ return file.data;
637
1764
  }
638
-
639
- // src/signed-url.ts
640
- import { Buffer } from "buffer";
641
- import { createHmac, timingSafeEqual } from "crypto";
642
- import process2 from "process";
643
- var ALG = "HS256";
644
- function getAppKey() {
645
- const k = process2.env.APP_KEY;
646
- if (!k || k.length < 16) {
647
- if (process2.env.APP_ENV === "production" || process2.env.NODE_ENV === "production") {
648
- throw new Error("[storage/signed-url] APP_KEY is missing or too short (need \u226516 chars). Cannot sign URL.");
649
- }
1765
+ async function writeFile(path, data) {
1766
+ if (typeof path === "string") {
1767
+ const dirPath = dirname(path);
1768
+ if (!await existsSync(dirPath))
1769
+ await createFolder(dirPath);
1770
+ return await Bun.write(Bun.file(path), data);
650
1771
  }
651
- return k || "stacks-default-key-dev-only-do-not-use-prod";
652
- }
653
- function base64UrlEncode(buf) {
654
- return buf.toString("base64url");
655
- }
656
- function base64UrlDecode(str) {
657
- return Buffer.from(str, "base64url");
1772
+ return await Bun.write(path, data);
658
1773
  }
659
- function normalizeExpiry(expiresIn) {
660
- if (expiresIn instanceof Date)
661
- return Math.floor(expiresIn.getTime() / 1000);
662
- return Math.floor(Date.now() / 1000) + Math.floor(expiresIn);
1774
+ async function writeJsonFile(file) {
1775
+ let json = JSON.stringify(file.data, undefined, file.indent);
1776
+ if (file.newline)
1777
+ json += file.newline;
1778
+ return writeTextFile({ ...file, data: json });
663
1779
  }
664
- function createSignedStorageToken(path2, options) {
665
- const exp = normalizeExpiry(options.expiresIn);
666
- const iat = Math.floor(Date.now() / 1000);
667
- const header = { alg: ALG, typ: "JWT" };
668
- const payload = {
669
- iss: options.issuer || "stacks",
670
- iat,
671
- exp,
672
- path: path2
673
- };
674
- const headerPart = base64UrlEncode(Buffer.from(JSON.stringify(header)));
675
- const payloadPart = base64UrlEncode(Buffer.from(JSON.stringify(payload)));
676
- const signingInput = `${headerPart}.${payloadPart}`;
677
- const sig = base64UrlEncode(createHmac("sha256", getAppKey()).update(signingInput).digest());
678
- return `${signingInput}.${sig}`;
1780
+ function readTextFile(name, cwd) {
1781
+ return new Promise((resolve, reject) => {
1782
+ let filePath;
1783
+ if (cwd)
1784
+ filePath = join4(cwd, name);
1785
+ else
1786
+ filePath = name;
1787
+ fs.readFile(filePath, "utf8", (err2, text) => {
1788
+ if (err2) {
1789
+ reject(err2);
1790
+ } else {
1791
+ resolve({
1792
+ path: filePath,
1793
+ data: text
1794
+ });
1795
+ }
1796
+ });
1797
+ });
679
1798
  }
680
- function verifySignedStorageToken(token, requestedPath) {
681
- if (typeof token !== "string") {
682
- return { valid: false, reason: "malformed" };
683
- }
684
- const parts = token.split(".");
685
- if (parts.length !== 3) {
686
- return { valid: false, reason: "malformed" };
687
- }
688
- const headerPart = parts[0];
689
- const payloadPart = parts[1];
690
- const sigPart = parts[2];
691
- const signingInput = `${headerPart}.${payloadPart}`;
692
- const expectedSig = createHmac("sha256", getAppKey()).update(signingInput).digest();
693
- let providedSig;
694
- try {
695
- providedSig = base64UrlDecode(sigPart);
696
- } catch {
697
- return { valid: false, reason: "malformed" };
698
- }
699
- if (providedSig.length !== expectedSig.length || !timingSafeEqual(providedSig, expectedSig)) {
700
- return { valid: false, reason: "bad_signature" };
701
- }
702
- let claims;
1799
+ async function writeTextFile(file) {
1800
+ return await Bun.write(file.path, file.data);
1801
+ }
1802
+ function isFile(path) {
1803
+ return fs.existsSync(path);
1804
+ }
1805
+ function doesExist(path) {
1806
+ return isFile(path) || isFolder(path);
1807
+ }
1808
+ function doesNotExist(path) {
1809
+ return !isFile(path) && !isFolder(path);
1810
+ }
1811
+ function hasFiles(folder) {
703
1812
  try {
704
- claims = JSON.parse(base64UrlDecode(payloadPart).toString("utf8"));
705
- } catch {
706
- return { valid: false, reason: "malformed" };
707
- }
708
- const now = Math.floor(Date.now() / 1000);
709
- if (typeof claims.exp !== "number" || now >= claims.exp) {
710
- return { valid: false, reason: "expired" };
711
- }
712
- if (claims.path !== requestedPath) {
713
- return { valid: false, reason: "path_mismatch" };
1813
+ return fs.readdirSync(folder).length > 0;
1814
+ } catch (err2) {
1815
+ log2.debug(`Error reading folder: ${folder}`, err2);
1816
+ return false;
714
1817
  }
715
- return { valid: true, claims };
716
1818
  }
717
-
718
- // src/adapters/local.ts
719
- class LocalStorageAdapter {
720
- root;
721
- constructor(config = {}) {
722
- this.root = config.root || process.cwd();
1819
+ function hasComponents() {
1820
+ return hasFiles(p.componentsPath());
1821
+ }
1822
+ function hasFunctions() {
1823
+ return hasFiles(p.functionsPath());
1824
+ }
1825
+ function deleteFiles(dir, exclude = []) {
1826
+ if (fs.existsSync(dir)) {
1827
+ fs.readdirSync(dir).forEach((file) => {
1828
+ const p2 = join4(dir, file);
1829
+ if (fs.statSync(p2).isDirectory()) {
1830
+ if (fs.readdirSync(p2).length === 0)
1831
+ fs.rmSync(p2, { recursive: true, force: true });
1832
+ else
1833
+ deleteFiles(p2, exclude);
1834
+ } else if (!contains2(p2, exclude)) {
1835
+ fs.rmSync(p2);
1836
+ }
1837
+ });
723
1838
  }
724
- resolvePath(path2) {
725
- const resolved = join5(this.root, path2);
726
- const rel = relative(this.root, resolved);
727
- if (rel.startsWith("..") || rel.startsWith("../") || rel.startsWith("..\\")) {
728
- throw new Error(`Path traversal detected: '${path2}' resolves outside storage root`);
729
- }
730
- return resolved;
1839
+ }
1840
+ function getFiles(dir, exclude = []) {
1841
+ let results = [];
1842
+ const list = fs.readdirSync(dir);
1843
+ list.forEach((file) => {
1844
+ file = join4(dir, file);
1845
+ const stat = fs.statSync(file);
1846
+ if (stat.isDirectory())
1847
+ results = results.concat(getFiles(file, exclude));
1848
+ else if (!contains2(file, exclude))
1849
+ results.push(file);
1850
+ });
1851
+ return results;
1852
+ }
1853
+ function put(path, contents) {
1854
+ const dirPath = dirname(path);
1855
+ if (!fs.existsSync(dirPath))
1856
+ fs.mkdirSync(dirPath, { recursive: true });
1857
+ fs.writeFileSync(path, contents, "utf-8");
1858
+ }
1859
+ async function get(path) {
1860
+ return Bun.file(path).text();
1861
+ }
1862
+ var files = {
1863
+ readJsonFile,
1864
+ readPackageJson,
1865
+ readTextFile,
1866
+ writeJsonFile,
1867
+ writeTextFile,
1868
+ hasFiles,
1869
+ hasComponents,
1870
+ hasFunctions,
1871
+ deleteFiles,
1872
+ getFiles,
1873
+ put,
1874
+ get
1875
+ };
1876
+ // src/hash.ts
1877
+ import { createHash } from "crypto";
1878
+ import { path as p2 } from "@stacksjs/path";
1879
+ function hashFileOrDirectory(path, hash) {
1880
+ if (!fs.existsSync(path)) {
1881
+ console.error(`Path does not exist: ${path}`);
1882
+ return;
731
1883
  }
732
- async write(path2, contents) {
733
- const fullPath = this.resolvePath(path2);
734
- const dir = dirname3(fullPath);
735
- await mkdir(dir, { recursive: true });
736
- if (typeof contents === "string") {
737
- await writeFile2(fullPath, contents, "utf8");
738
- } else if (contents instanceof Buffer2) {
739
- await writeFile2(fullPath, contents);
740
- } else if (contents instanceof Uint8Array) {
741
- await writeFile2(fullPath, contents);
742
- } else {
743
- const writeStream = createWriteStream(fullPath);
744
- await pipeline(contents, writeStream);
1884
+ if (fs.statSync(path).isDirectory()) {
1885
+ const files2 = fs.readdirSync(path);
1886
+ for (const file of files2) {
1887
+ const filePath = p2.join(path, file);
1888
+ hashFileOrDirectory(filePath, hash);
745
1889
  }
1890
+ } else {
1891
+ hash.update(fs.readFileSync(path));
746
1892
  }
747
- async read(path2) {
748
- const fullPath = this.resolvePath(path2);
749
- return await readFile(fullPath);
750
- }
751
- async readToString(path2) {
752
- const fullPath = this.resolvePath(path2);
753
- return await readFile(fullPath, "utf8");
754
- }
755
- async readToBuffer(path2) {
756
- const fullPath = this.resolvePath(path2);
757
- return await readFile(fullPath);
758
- }
759
- async readToUint8Array(path2) {
760
- const fullPath = this.resolvePath(path2);
761
- const buffer = await readFile(fullPath);
762
- return new Uint8Array(buffer);
763
- }
764
- async deleteFile(path2) {
765
- const fullPath = this.resolvePath(path2);
766
- await unlink(fullPath);
767
- }
768
- async deleteDirectory(path2) {
769
- const fullPath = this.resolvePath(path2);
770
- await rm(fullPath, { recursive: true, force: true });
771
- }
772
- async createDirectory(path2) {
773
- const fullPath = this.resolvePath(path2);
774
- await mkdir(fullPath, { recursive: true });
775
- }
776
- async moveFile(from, to) {
777
- const fromPath = this.resolvePath(from);
778
- const toPath = this.resolvePath(to);
779
- const toDir = dirname3(toPath);
780
- await mkdir(toDir, { recursive: true });
781
- await rename2(fromPath, toPath);
782
- }
783
- async copyFile(from, to) {
784
- const fromPath = this.resolvePath(from);
785
- const toPath = this.resolvePath(to);
786
- const toDir = dirname3(toPath);
787
- await mkdir(toDir, { recursive: true });
788
- await copyFile2(fromPath, toPath);
789
- }
790
- async stat(path2) {
791
- const fullPath = this.resolvePath(path2);
792
- const stats = await lstat(fullPath);
793
- return {
794
- path: path2,
795
- type: stats.isDirectory() ? "directory" : "file",
796
- visibility: await this.visibility(path2),
797
- size: stats.size,
798
- lastModified: stats.mtimeMs,
799
- mimeType: stats.isFile() ? await this.detectMimeType(fullPath) : undefined
800
- };
801
- }
802
- list(path2, options = {}) {
803
- return this.createAsyncIterator(path2, options.deep || false);
804
- }
805
- async* createAsyncIterator(path2, deep) {
806
- const fullPath = this.resolvePath(path2);
1893
+ }
1894
+ function hashDirectory(directory) {
1895
+ const hash = createHash("sha256");
1896
+ hashFileOrDirectory(directory, hash);
1897
+ return hash.digest("hex");
1898
+ }
1899
+ function hashPath(path) {
1900
+ const hash = createHash("sha256");
1901
+ hashFileOrDirectory(path, hash);
1902
+ return hash.digest("hex");
1903
+ }
1904
+ function hashPaths(paths) {
1905
+ const hash = createHash("sha256");
1906
+ const pathsArray = Array.isArray(paths) ? paths : [paths];
1907
+ for (const path of pathsArray)
1908
+ hashFileOrDirectory(path, hash);
1909
+ return hash.digest("hex");
1910
+ }
1911
+ // src/helpers.ts
1912
+ import { fileURLToPath } from "url";
1913
+ import { dirname as dirname2 } from "@stacksjs/path";
1914
+ var __dirname = "/home/runner/work/stacks/stacks/storage/framework/core/storage/src";
1915
+ var _dirname = typeof __dirname !== "undefined" ? __dirname : dirname2(fileURLToPath(import.meta.url));
1916
+ function updateConfigFile(filePath, newConfig) {
1917
+ return new Promise((resolve, reject) => {
1918
+ let config;
807
1919
  try {
808
- await access(fullPath, constants.R_OK);
809
- } catch {
1920
+ config = JSON.parse(fs.readFileSync(filePath, "utf8"));
1921
+ } catch (error) {
1922
+ reject(new Error(`Failed to parse config file "${filePath}": ${error.message}`));
810
1923
  return;
811
1924
  }
812
- const entries = await this.readDirectoryRecursive(fullPath, deep);
813
- yield* createDirectoryListing(entries);
814
- }
815
- async readDirectoryRecursive(dirPath, deep) {
816
- const entries = [];
1925
+ for (const key in newConfig)
1926
+ config[key] = newConfig[key];
817
1927
  try {
818
- const items = await readdir(dirPath, { withFileTypes: true });
819
- for (const item of items) {
820
- const itemPath = join5(dirPath, item.name);
821
- const relativePath = relative(this.root, itemPath);
822
- entries.push({
823
- path: relativePath,
824
- type: item.isDirectory() ? "directory" : "file"
825
- });
826
- if (deep && item.isDirectory()) {
827
- const subEntries = await this.readDirectoryRecursive(itemPath, true);
828
- entries.push(...subEntries);
1928
+ fs.writeFileSync(filePath, JSON.stringify(config, null, 2));
1929
+ resolve();
1930
+ } catch (error) {
1931
+ reject(error);
1932
+ }
1933
+ });
1934
+ }
1935
+ var helpers = {
1936
+ _dirname,
1937
+ updateConfigFile
1938
+ };
1939
+ // src/storage.ts
1940
+ var exports_storage = {};
1941
+ __export(exports_storage, {
1942
+ zip: () => zip,
1943
+ writeTextFile: () => writeTextFile,
1944
+ writeJsonFile: () => writeJsonFile,
1945
+ writeFileSync: () => writeFileSync,
1946
+ writeFile: () => writeFile,
1947
+ watchFile: () => watchFile,
1948
+ updateConfigFile: () => updateConfigFile,
1949
+ unzip: () => unzip,
1950
+ unarchive: () => unarchive,
1951
+ setVisibility: () => setVisibility,
1952
+ rename: () => rename,
1953
+ readTextFile: () => readTextFile,
1954
+ readPackageJson: () => readPackageJson,
1955
+ readJsonFile: () => readJsonFile,
1956
+ readFileSync: () => readFileSync,
1957
+ put: () => put,
1958
+ move: () => move,
1959
+ mkdirSync: () => mkdirSync,
1960
+ isFolder: () => isFolder,
1961
+ isDirectoryEmpty: () => isDirectoryEmpty,
1962
+ isDir: () => isDir,
1963
+ inflateSync: () => inflateSync,
1964
+ helpers: () => helpers,
1965
+ hasFunctions: () => hasFunctions,
1966
+ hasFiles: () => hasFiles,
1967
+ hasComponents: () => hasComponents,
1968
+ gzipSync: () => gzipSync,
1969
+ gunzipSync: () => gunzipSync,
1970
+ getFolders: () => getFolders,
1971
+ getFiles: () => getFiles,
1972
+ get: () => get,
1973
+ fsWatch: () => fsWatch,
1974
+ fs: () => fs,
1975
+ folders: () => folders,
1976
+ files: () => files,
1977
+ existsSync: () => existsSync,
1978
+ exists: () => exists,
1979
+ doesNotExist: () => doesNotExist,
1980
+ doesFolderExist: () => doesFolderExist,
1981
+ doesExist: () => doesExist,
1982
+ deleteGlob: () => deleteGlob,
1983
+ deleteFolder: () => deleteFolder,
1984
+ deleteFiles: () => deleteFiles,
1985
+ deleteFile: () => deleteFile,
1986
+ deleteEmptyFolders: () => deleteEmptyFolders,
1987
+ deleteEmptyFolder: () => deleteEmptyFolder,
1988
+ del: () => del,
1989
+ deflateSync: () => deflateSync,
1990
+ decompress: () => decompress,
1991
+ createFolder: () => createFolder,
1992
+ copyFolder: () => copyFolder,
1993
+ copyFile: () => copyFile,
1994
+ copy: () => copy,
1995
+ compress: () => compress,
1996
+ archive: () => archive,
1997
+ _dirname: () => _dirname
1998
+ });
1999
+
2000
+ // src/move.ts
2001
+ import { err as err2, handleError as handleError2, ok as ok2 } from "@stacksjs/error-handling";
2002
+ import { log as log3 } from "@stacksjs/logging";
2003
+ import { path } from "@stacksjs/path";
2004
+ async function move(src, dest, options) {
2005
+ try {
2006
+ if (Array.isArray(src)) {
2007
+ const errors = [];
2008
+ const operations = src.map(async (file) => {
2009
+ const from2 = file;
2010
+ const to2 = path.resolve(dest, path.basename(file));
2011
+ const result2 = await rename(from2, to2, options);
2012
+ if (result2.isErr) {
2013
+ log3.error(result2.error);
2014
+ errors.push(result2.error);
829
2015
  }
830
- }
831
- } catch (error) {
832
- if (error?.code !== "EACCES" && error?.code !== "EPERM") {
833
- throw error;
834
- }
2016
+ });
2017
+ await Promise.all(operations);
2018
+ if (errors.length > 0)
2019
+ return err2(handleError2(errors[0]));
2020
+ return ok2({ message: "Files moved successfully" });
835
2021
  }
836
- return entries;
837
- }
838
- async changeVisibility(_path, _visibility) {}
839
- async visibility(_path) {
840
- return "private";
841
- }
842
- async fileExists(path2) {
843
- const fullPath = this.resolvePath(path2);
844
- try {
845
- const stats = await lstat(fullPath);
846
- return stats.isFile();
847
- } catch {
848
- return false;
2022
+ const from = src;
2023
+ const to = dest;
2024
+ const result = await rename(from, to, options);
2025
+ if (result.isErr) {
2026
+ log3.error(result.error);
2027
+ return err2(handleError2(result.error));
849
2028
  }
2029
+ return ok2({ message: "File moved successfully" });
2030
+ } catch (error) {
2031
+ return err2(handleError2(error));
850
2032
  }
851
- async directoryExists(path2) {
852
- const fullPath = this.resolvePath(path2);
2033
+ }
2034
+ async function rename(from, to, options) {
2035
+ return new Promise((resolve, reject) => {
853
2036
  try {
854
- const stats = await lstat(fullPath);
855
- return stats.isDirectory();
856
- } catch {
857
- return false;
2037
+ const dir = path.dirname(to);
2038
+ if (!fs.existsSync(dir))
2039
+ fs.mkdirSync(dir, { recursive: true });
2040
+ if (!fs.existsSync(from))
2041
+ return reject(err2(new Error(`File or directory does not exist: ${from}`)));
2042
+ if (fs.existsSync(to)) {
2043
+ if (!options?.overwrite)
2044
+ return reject(err2(new Error(`File or directory already exists: ${to}`)));
2045
+ fs.rmSync(to, { recursive: true, force: true });
2046
+ }
2047
+ fs.renameSync(from, to);
2048
+ return resolve(ok2({ message: "File moved successfully" }));
2049
+ } catch (error) {
2050
+ if (error.code === "ENOENT")
2051
+ log3.error(`File or directory does not exist
2052
+
2053
+ `, error);
2054
+ else
2055
+ log3.error(error);
2056
+ return reject(err2(new Error(error)));
858
2057
  }
2058
+ });
2059
+ }
2060
+ // src/visibility.ts
2061
+ function setVisibility() {
2062
+ return "wip";
2063
+ }
2064
+ // src/zip.ts
2065
+ import { runCommand } from "@stacksjs/cli";
2066
+ function shellEscape(_arg) {
2067
+ return `'${_arg.replace(/'/g, "'\\''")}'`;
2068
+ }
2069
+ async function zip(from, to, options) {
2070
+ const toPath = to || "archive.zip";
2071
+ if (Array.isArray(from)) {
2072
+ const fromPath = from.map((f) => shellEscape(f)).join(" ");
2073
+ return runCommand(`zip -r ${shellEscape(toPath)} ${fromPath}`, options);
859
2074
  }
860
- async publicUrl(path2, options = {}) {
861
- const domain = options.domain || "http://localhost";
862
- return `${domain}/${path2}`;
863
- }
864
- async temporaryUrl(path2, options) {
865
- const expiry = normalizeExpiryToDate(options.expiresIn);
866
- const payload = `${path2}:${expiry.getTime()}`;
867
- const appKey = process.env.APP_KEY || "stacks-default-key";
868
- const signature = createHmac2("sha256", appKey).update(payload).digest("hex");
869
- const token = Buffer2.from(`${payload}:${signature}`).toString("base64url");
870
- return `http://localhost/temp/${token}`;
871
- }
872
- async signedUrl(path2, options) {
873
- const token = createSignedStorageToken(path2, options);
874
- const baseUrl = (options.baseUrl || process.env.APP_URL || "http://localhost").replace(/\/$/, "");
875
- return `${baseUrl}/__storage/${encodeURIComponent(path2)}?token=${token}`;
876
- }
877
- async checksum(path2, options = {}) {
878
- const algorithm = options.algorithm || "sha256";
879
- const fullPath = this.resolvePath(path2);
880
- const content = await readFile(fullPath);
881
- const hasher = new Bun.CryptoHasher(algorithm);
882
- hasher.update(content);
883
- return hasher.digest("hex");
884
- }
885
- async mimeType(path2, options = {}) {
886
- const fullPath = this.resolvePath(path2);
887
- return await this.detectMimeType(fullPath);
888
- }
889
- async detectMimeType(filePath) {
890
- const ext = basename(filePath).split(".").pop()?.toLowerCase();
891
- const mimeTypes = {
892
- txt: "text/plain",
893
- html: "text/html",
894
- css: "text/css",
895
- js: "application/javascript",
896
- json: "application/json",
897
- xml: "application/xml",
898
- pdf: "application/pdf",
899
- zip: "application/zip",
900
- jpg: "image/jpeg",
901
- jpeg: "image/jpeg",
902
- png: "image/png",
903
- gif: "image/gif",
904
- svg: "image/svg+xml",
905
- mp4: "video/mp4",
906
- mp3: "audio/mpeg",
907
- wav: "audio/wav"
908
- };
909
- return mimeTypes[ext || ""] || "application/octet-stream";
910
- }
911
- async lastModified(path2) {
912
- const stats = await this.stat(path2);
913
- return stats.lastModified;
914
- }
915
- async fileSize(path2) {
916
- const stats = await this.stat(path2);
917
- return stats.size;
918
- }
2075
+ return runCommand(`zip -r ${shellEscape(toPath)} ${shellEscape(from)}`, options);
919
2076
  }
920
- function createLocalStorage(config = {}) {
921
- return new LocalStorageAdapter(config);
2077
+ async function unzip(paths) {
2078
+ if (Array.isArray(paths))
2079
+ return runCommand(`unzip ${paths.map((p3) => shellEscape(p3)).join(" ")}`);
2080
+ return runCommand(`unzip ${shellEscape(paths)}`);
2081
+ }
2082
+ function archive(paths) {
2083
+ return zip(paths);
2084
+ }
2085
+ function unarchive(paths) {
2086
+ return unzip(paths);
2087
+ }
2088
+ function compress(paths) {
2089
+ return zip(paths);
2090
+ }
2091
+ function decompress(paths) {
2092
+ return unzip(paths);
2093
+ }
2094
+ function gzipSync(data, options) {
2095
+ return Bun.gzipSync(data, options);
2096
+ }
2097
+ function gunzipSync(data) {
2098
+ return Bun.gunzipSync(data);
2099
+ }
2100
+ function deflateSync(data, options) {
2101
+ return Bun.deflateSync(data, options);
922
2102
  }
2103
+ function inflateSync(data) {
2104
+ return Bun.inflateSync(data);
2105
+ }
2106
+ // src/adapters/index.ts
2107
+ init_local();
2108
+
923
2109
  // src/adapters/memory.ts
2110
+ init_types();
924
2111
  import { Buffer as Buffer3 } from "buffer";
925
2112
  import { basename as basename2 } from "path";
2113
+
926
2114
  class InMemoryStorageAdapter {
927
2115
  files;
928
2116
  directories;
@@ -947,7 +2135,11 @@ class InMemoryStorageAdapter {
947
2135
  } else if (contents instanceof Uint8Array) {
948
2136
  return contents;
949
2137
  } else {
950
- const reader = contents.getReader();
2138
+ const stream = contents;
2139
+ if (typeof stream.getReader !== "function") {
2140
+ throw new TypeError("[storage/memory] contents must be a web-standard ReadableStream " + "(with .getReader()), not a Node stream.Readable. " + "Convert via Readable.toWeb(nodeStream) before passing.");
2141
+ }
2142
+ const reader = stream.getReader.call(contents);
951
2143
  const chunks = [];
952
2144
  while (true) {
953
2145
  const { done, value } = await reader.read();
@@ -973,20 +2165,81 @@ class InMemoryStorageAdapter {
973
2165
  await this.createDirectory(dirPath);
974
2166
  }
975
2167
  const data = await this.contentsToUint8Array(contents);
2168
+ const lastModified = Date.now();
2169
+ const mimeType = this.detectMimeType(normalized);
2170
+ this.files.set(normalized, {
2171
+ contents: data,
2172
+ visibility: "private",
2173
+ mimeType,
2174
+ lastModified
2175
+ });
2176
+ return {
2177
+ path: normalized,
2178
+ size: data.length,
2179
+ contentType: mimeType,
2180
+ lastModified
2181
+ };
2182
+ }
2183
+ async read(path2) {
2184
+ const normalized = this.normalizePath(path2);
2185
+ const file = this.files.get(normalized);
2186
+ if (!file) {
2187
+ throw new Error(`File not found: ${path2}`);
2188
+ }
2189
+ return file.contents;
2190
+ }
2191
+ async getStream(path2, _options) {
2192
+ const normalized = this.normalizePath(path2);
2193
+ const file = this.files.get(normalized);
2194
+ if (!file)
2195
+ throw new Error(`File not found: ${path2}`);
2196
+ const bytes = file.contents;
2197
+ return new ReadableStream({
2198
+ start(controller) {
2199
+ controller.enqueue(bytes);
2200
+ controller.close();
2201
+ }
2202
+ });
2203
+ }
2204
+ async putStream(path2, stream, options) {
2205
+ const normalized = this.normalizePath(path2);
2206
+ const dirPath = this.getDirectoryPath(normalized);
2207
+ if (dirPath)
2208
+ await this.createDirectory(dirPath);
2209
+ const chunks = [];
2210
+ const reader = stream.getReader();
2211
+ const abort = options?.signal;
2212
+ try {
2213
+ while (true) {
2214
+ if (abort?.aborted)
2215
+ throw new Error("aborted");
2216
+ const { done, value } = await reader.read();
2217
+ if (done)
2218
+ break;
2219
+ if (value)
2220
+ chunks.push(value);
2221
+ }
2222
+ } finally {
2223
+ try {
2224
+ reader.releaseLock();
2225
+ } catch {}
2226
+ }
2227
+ const totalLength = chunks.reduce((sum, c) => sum + c.length, 0);
2228
+ const data = new Uint8Array(totalLength);
2229
+ let offset = 0;
2230
+ for (const c of chunks) {
2231
+ data.set(c, offset);
2232
+ offset += c.length;
2233
+ }
2234
+ const lastModified = Date.now();
2235
+ const mimeType = options?.contentType ?? this.detectMimeType(normalized);
976
2236
  this.files.set(normalized, {
977
2237
  contents: data,
978
2238
  visibility: "private",
979
- mimeType: this.detectMimeType(normalized),
980
- lastModified: Date.now()
2239
+ mimeType,
2240
+ lastModified
981
2241
  });
982
- }
983
- async read(path2) {
984
- const normalized = this.normalizePath(path2);
985
- const file = this.files.get(normalized);
986
- if (!file) {
987
- throw new Error(`File not found: ${path2}`);
988
- }
989
- return file.contents;
2242
+ return { path: normalized, size: data.length, contentType: mimeType, lastModified };
990
2243
  }
991
2244
  async readToString(path2) {
992
2245
  const data = await this.read(path2);
@@ -1163,8 +2416,8 @@ class InMemoryStorageAdapter {
1163
2416
  return this.directories.has(normalized) || normalized === "";
1164
2417
  }
1165
2418
  async publicUrl(path2, options = {}) {
1166
- const domain = options.domain || "http://localhost";
1167
- return `${domain}/${this.normalizePath(path2)}`;
2419
+ const base = (options.domain || process.env.APP_URL || "http://localhost").replace(/\/$/, "");
2420
+ return `${base}/${this.normalizePath(path2)}`;
1168
2421
  }
1169
2422
  async temporaryUrl(path2, options) {
1170
2423
  const expiry = normalizeExpiryToDate(options.expiresIn);
@@ -1228,261 +2481,18 @@ class InMemoryStorageAdapter {
1228
2481
  function createMemoryStorage() {
1229
2482
  return new InMemoryStorageAdapter;
1230
2483
  }
1231
- // src/adapters/s3.ts
1232
- import { Buffer as Buffer4 } from "buffer";
1233
- import { basename as basename3 } from "path";
1234
- class S3StorageAdapter {
1235
- client;
1236
- bucket;
1237
- prefix;
1238
- region;
1239
- constructor(client, config) {
1240
- this.client = client;
1241
- this.bucket = config.bucket || "";
1242
- this.prefix = config.prefix || "";
1243
- this.region = config.region || "us-east-1";
1244
- if (!this.bucket) {
1245
- throw new Error("S3 bucket name is required");
1246
- }
1247
- }
1248
- prefixPath(path2) {
1249
- if (!this.prefix)
1250
- return path2;
1251
- return `${this.prefix}/${path2}`.replace(/\/+/g, "/");
1252
- }
1253
- stripPrefix(path2) {
1254
- if (!this.prefix)
1255
- return path2;
1256
- const prefixWithSlash = `${this.prefix}/`;
1257
- return path2.startsWith(prefixWithSlash) ? path2.slice(prefixWithSlash.length) : path2;
1258
- }
1259
- async contentsToBuffer(contents) {
1260
- if (typeof contents === "string") {
1261
- return Buffer4.from(contents, "utf8");
1262
- } else if (contents instanceof Buffer4) {
1263
- return contents;
1264
- } else if (contents instanceof Uint8Array) {
1265
- return Buffer4.from(contents);
1266
- } else {
1267
- const reader = contents.getReader();
1268
- const chunks = [];
1269
- while (true) {
1270
- const { done, value } = await reader.read();
1271
- if (done)
1272
- break;
1273
- if (value)
1274
- chunks.push(value);
1275
- }
1276
- return Buffer4.concat(chunks.map((c) => Buffer4.from(c)));
1277
- }
1278
- }
1279
- async write(path2, contents) {
1280
- const key = this.prefixPath(path2);
1281
- const body = await this.contentsToBuffer(contents);
1282
- await this.client.putObject({
1283
- bucket: this.bucket,
1284
- key,
1285
- body,
1286
- contentType: this.detectMimeType(path2)
1287
- });
1288
- }
1289
- async read(path2) {
1290
- const key = this.prefixPath(path2);
1291
- const response = await this.client.getObject(this.bucket, key);
1292
- if (!response) {
1293
- throw new Error(`Failed to read file: ${path2}`);
1294
- }
1295
- return Buffer4.from(response);
1296
- }
1297
- async readToString(path2) {
1298
- const key = this.prefixPath(path2);
1299
- const response = await this.client.getObject(this.bucket, key);
1300
- if (!response) {
1301
- throw new Error(`Failed to read file: ${path2}`);
1302
- }
1303
- return response;
1304
- }
1305
- async readToBuffer(path2) {
1306
- const contents = await this.read(path2);
1307
- return contents;
1308
- }
1309
- async readToUint8Array(path2) {
1310
- const buffer = await this.readToBuffer(path2);
1311
- return new Uint8Array(buffer);
1312
- }
1313
- async deleteFile(path2) {
1314
- const key = this.prefixPath(path2);
1315
- await this.client.deleteObject(this.bucket, key);
1316
- }
1317
- async deleteDirectory(path2) {
1318
- const prefix = this.prefixPath(path2);
1319
- const normalizedPrefix = prefix.endsWith("/") ? prefix : `${prefix}/`;
1320
- const objects = await this.client.listAllObjects({ bucket: this.bucket, prefix: normalizedPrefix });
1321
- const keys = objects.map((obj) => obj.Key).filter((k) => typeof k === "string");
1322
- if (keys.length === 0) {
1323
- return;
1324
- }
1325
- await this.client.deleteObjects(this.bucket, keys);
1326
- }
1327
- async createDirectory(_path) {}
1328
- async moveFile(from, to) {
1329
- await this.copyFile(from, to);
1330
- await this.deleteFile(from);
1331
- }
1332
- async copyFile(from, to) {
1333
- const fromKey = this.prefixPath(from);
1334
- const toKey = this.prefixPath(to);
1335
- await this.client.copyObject({
1336
- sourceBucket: this.bucket,
1337
- sourceKey: fromKey,
1338
- destinationBucket: this.bucket,
1339
- destinationKey: toKey
1340
- });
1341
- }
1342
- async stat(path2) {
1343
- const key = this.prefixPath(path2);
1344
- const result = await this.client.headObject(this.bucket, key);
1345
- if (!result) {
1346
- throw new Error(`File not found: ${path2}`);
1347
- }
1348
- return {
1349
- path: path2,
1350
- type: "file",
1351
- visibility: "private",
1352
- size: result.ContentLength || 0,
1353
- lastModified: result.LastModified ? new Date(result.LastModified).getTime() : Date.now(),
1354
- mimeType: result.ContentType
1355
- };
1356
- }
1357
- list(path2, options = {}) {
1358
- return this.createAsyncIterator(path2, options.deep || false);
1359
- }
1360
- async* createAsyncIterator(path2, deep) {
1361
- const prefix = this.prefixPath(path2);
1362
- const normalizedPrefix = prefix ? `${prefix}/` : undefined;
1363
- if (deep) {
1364
- const objects = await this.client.listAllObjects({ bucket: this.bucket, prefix: normalizedPrefix });
1365
- for (const obj of objects) {
1366
- yield {
1367
- path: this.stripPrefix(obj.Key),
1368
- type: "file"
1369
- };
1370
- }
1371
- } else {
1372
- let continuationToken;
1373
- do {
1374
- const result = await this.client.listObjects({
1375
- bucket: this.bucket,
1376
- prefix: normalizedPrefix,
1377
- continuationToken
1378
- });
1379
- for (const obj of result.objects || []) {
1380
- yield {
1381
- path: this.stripPrefix(obj.Key),
1382
- type: "file"
1383
- };
1384
- }
1385
- continuationToken = result.nextContinuationToken;
1386
- } while (continuationToken);
1387
- }
1388
- }
1389
- async changeVisibility(_path, _visibility) {}
1390
- async visibility(_path) {
1391
- return "private";
1392
- }
1393
- async fileExists(path2) {
1394
- const key = this.prefixPath(path2);
1395
- try {
1396
- const result = await this.client.headObject(this.bucket, key);
1397
- return !!result;
1398
- } catch (error) {
1399
- if (!error.message?.includes("404") && !error.message?.includes("NoSuchKey") && !error.message?.includes("NotFound")) {
1400
- console.debug(`[s3] Unexpected error checking file existence for ${path2}: ${error.message}`);
1401
- }
1402
- return false;
1403
- }
1404
- }
1405
- async directoryExists(path2) {
1406
- const prefix = this.prefixPath(path2);
1407
- const result = await this.client.listObjects({
1408
- bucket: this.bucket,
1409
- prefix: `${prefix}/`,
1410
- maxKeys: 1
1411
- });
1412
- return (result.objects || []).length > 0;
1413
- }
1414
- async publicUrl(path2, options = {}) {
1415
- const key = this.prefixPath(path2);
1416
- const domain = options.domain || `https://${this.bucket}.s3.${this.region}.amazonaws.com`;
1417
- return `${domain}/${key}`;
1418
- }
1419
- async temporaryUrl(path2, options) {
1420
- const key = this.prefixPath(path2);
1421
- const expiresIn = Math.floor(normalizeExpiryToMilliseconds(options.expiresIn) / 1000);
1422
- const MIN_EXPIRY = 60;
1423
- const MAX_EXPIRY = 7 * 24 * 60 * 60;
1424
- if (!Number.isFinite(expiresIn) || expiresIn < MIN_EXPIRY || expiresIn > MAX_EXPIRY) {
1425
- throw new RangeError(`[storage/s3] temporaryUrl expiresIn must be between 60s and 7 days (got ${expiresIn}s)`);
1426
- }
1427
- return await this.client.getSignedUrl({
1428
- bucket: this.bucket,
1429
- key,
1430
- expiresIn,
1431
- operation: "getObject"
1432
- });
1433
- }
1434
- async signedUrl(path2, options) {
1435
- return this.temporaryUrl(path2, { expiresIn: options.expiresIn });
1436
- }
1437
- async checksum(path2, options = {}) {
1438
- const algorithm = options.algorithm || "sha256";
1439
- const content = await this.readToUint8Array(path2);
1440
- const hasher = new Bun.CryptoHasher(algorithm);
1441
- hasher.update(content);
1442
- return hasher.digest("hex");
1443
- }
1444
- async mimeType(path2, _options = {}) {
1445
- const stats = await this.stat(path2);
1446
- return stats.mimeType || this.detectMimeType(path2);
1447
- }
1448
- detectMimeType(path2) {
1449
- const ext = basename3(path2).split(".").pop()?.toLowerCase();
1450
- const mimeTypes = {
1451
- txt: "text/plain",
1452
- html: "text/html",
1453
- css: "text/css",
1454
- js: "application/javascript",
1455
- json: "application/json",
1456
- xml: "application/xml",
1457
- pdf: "application/pdf",
1458
- zip: "application/zip",
1459
- jpg: "image/jpeg",
1460
- jpeg: "image/jpeg",
1461
- png: "image/png",
1462
- gif: "image/gif",
1463
- svg: "image/svg+xml",
1464
- mp4: "video/mp4",
1465
- mp3: "audio/mpeg",
1466
- wav: "audio/wav"
1467
- };
1468
- return mimeTypes[ext || ""] || "application/octet-stream";
1469
- }
1470
- async lastModified(path2) {
1471
- const stats = await this.stat(path2);
1472
- return stats.lastModified;
1473
- }
1474
- async fileSize(path2) {
1475
- const stats = await this.stat(path2);
1476
- return stats.size;
1477
- }
1478
- }
1479
- function createS3Storage(client, config) {
1480
- return new S3StorageAdapter(client, config);
1481
- }
2484
+
2485
+ // src/adapters/index.ts
2486
+ init_s3();
2487
+
1482
2488
  // src/adapters/bun.ts
1483
- import { Buffer as Buffer5 } from "buffer";
2489
+ init_types();
2490
+ init_signed_url();
2491
+ import { Buffer as Buffer6 } from "buffer";
1484
2492
  var {file, write: bunWrite } = globalThis.Bun;
2493
+ import { chmod as chmod2, lstat as lstat2 } from "fs/promises";
1485
2494
  import { dirname as dirname4, join as join6, relative as relative2 } from "path";
2495
+
1486
2496
  class BunStorageAdapter {
1487
2497
  root;
1488
2498
  constructor(config = {}) {
@@ -1502,12 +2512,16 @@ class BunStorageAdapter {
1502
2512
  await this.createDirectory(relative2(this.root, dir));
1503
2513
  if (typeof contents === "string") {
1504
2514
  await bunWrite(fullPath, contents);
1505
- } else if (contents instanceof Buffer5) {
2515
+ } else if (contents instanceof Buffer6) {
1506
2516
  await bunWrite(fullPath, contents);
1507
2517
  } else if (contents instanceof Uint8Array) {
1508
2518
  await bunWrite(fullPath, contents);
1509
2519
  } else {
1510
- const reader = contents.getReader();
2520
+ const stream = contents;
2521
+ if (typeof stream.getReader !== "function") {
2522
+ throw new TypeError("[storage/bun] contents must be a web-standard ReadableStream " + "(with .getReader()), not a Node stream.Readable. " + "Convert via Readable.toWeb(nodeStream) before passing.");
2523
+ }
2524
+ const reader = stream.getReader.call(contents);
1511
2525
  const chunks = [];
1512
2526
  while (true) {
1513
2527
  const { done, value } = await reader.read();
@@ -1525,6 +2539,12 @@ class BunStorageAdapter {
1525
2539
  }
1526
2540
  await bunWrite(fullPath, result);
1527
2541
  }
2542
+ const written = file(fullPath);
2543
+ return {
2544
+ path: path2,
2545
+ size: written.size,
2546
+ lastModified: written.lastModified
2547
+ };
1528
2548
  }
1529
2549
  async read(path2) {
1530
2550
  const fullPath = this.resolvePath(path2);
@@ -1532,7 +2552,46 @@ class BunStorageAdapter {
1532
2552
  if (!await bunFile.exists()) {
1533
2553
  throw new Error(`File not found: ${path2}`);
1534
2554
  }
1535
- return await bunFile.arrayBuffer().then((buf) => new Uint8Array(buf));
2555
+ return await bunFile.arrayBuffer().then((buf) => new Uint8Array(buf));
2556
+ }
2557
+ async getStream(path2, _options) {
2558
+ const fullPath = this.resolvePath(path2);
2559
+ const bunFile = file(fullPath);
2560
+ if (!await bunFile.exists())
2561
+ throw new Error(`File not found: ${path2}`);
2562
+ return bunFile.stream();
2563
+ }
2564
+ async putStream(path2, stream, options) {
2565
+ const fullPath = this.resolvePath(path2);
2566
+ const dir = dirname4(fullPath);
2567
+ await this.createDirectory(relative2(this.root, dir));
2568
+ try {
2569
+ const body = new Response(stream);
2570
+ const writePromise = bunWrite(fullPath, body);
2571
+ if (options?.signal) {
2572
+ const abortHandler = () => {};
2573
+ options.signal.addEventListener("abort", abortHandler, { once: true });
2574
+ try {
2575
+ await writePromise;
2576
+ } finally {
2577
+ options.signal.removeEventListener("abort", abortHandler);
2578
+ }
2579
+ } else {
2580
+ await writePromise;
2581
+ }
2582
+ } catch (err3) {
2583
+ try {
2584
+ await Bun.$.throws(false)`rm -f ${fullPath}`;
2585
+ } catch {}
2586
+ throw err3;
2587
+ }
2588
+ const written = file(fullPath);
2589
+ return {
2590
+ path: path2,
2591
+ size: written.size,
2592
+ contentType: options?.contentType,
2593
+ lastModified: written.lastModified
2594
+ };
1536
2595
  }
1537
2596
  async readToString(path2) {
1538
2597
  const fullPath = this.resolvePath(path2);
@@ -1549,7 +2608,7 @@ class BunStorageAdapter {
1549
2608
  throw new Error(`File not found: ${path2}`);
1550
2609
  }
1551
2610
  const arrayBuffer = await bunFile.arrayBuffer();
1552
- return Buffer5.from(arrayBuffer);
2611
+ return Buffer6.from(arrayBuffer);
1553
2612
  }
1554
2613
  async readToUint8Array(path2) {
1555
2614
  const fullPath = this.resolvePath(path2);
@@ -1627,9 +2686,18 @@ class BunStorageAdapter {
1627
2686
  }
1628
2687
  yield* createDirectoryListing(entries);
1629
2688
  }
1630
- async changeVisibility(_path, _visibility) {}
1631
- async visibility(_path) {
1632
- return "private";
2689
+ async changeVisibility(path2, vis) {
2690
+ const fullPath = this.resolvePath(path2);
2691
+ const stats = await lstat2(fullPath);
2692
+ const isDir2 = stats.isDirectory();
2693
+ const mode = vis === "public" ? isDir2 ? 493 : 420 : isDir2 ? 448 : 384;
2694
+ await chmod2(fullPath, mode);
2695
+ }
2696
+ async visibility(path2) {
2697
+ const fullPath = this.resolvePath(path2);
2698
+ const stats = await lstat2(fullPath);
2699
+ const perms = stats.mode & 511;
2700
+ return perms & 4 ? "public" : "private";
1633
2701
  }
1634
2702
  async fileExists(path2) {
1635
2703
  const fullPath = this.resolvePath(path2);
@@ -1651,7 +2719,7 @@ class BunStorageAdapter {
1651
2719
  }
1652
2720
  async temporaryUrl(path2, options) {
1653
2721
  const expiry = normalizeExpiryToDate(options.expiresIn);
1654
- const token = Buffer5.from(`${path2}:${expiry.getTime()}`).toString("base64url");
2722
+ const token = Buffer6.from(`${path2}:${expiry.getTime()}`).toString("base64url");
1655
2723
  return `http://localhost/temp/${token}`;
1656
2724
  }
1657
2725
  async signedUrl(path2, options) {
@@ -1691,23 +2759,173 @@ class BunStorageAdapter {
1691
2759
  function createBunStorage(config = {}) {
1692
2760
  return new BunStorageAdapter(config);
1693
2761
  }
2762
+ // src/adapters/scoped.ts
2763
+ var DEFAULT_SCOPE_PATTERN = /^[a-z0-9_-]+$/i;
2764
+
2765
+ class ScopedStorageAdapter {
2766
+ inner;
2767
+ scope;
2768
+ scopeWithSlash;
2769
+ constructor(inner, options) {
2770
+ const pattern = options.scopePattern ?? DEFAULT_SCOPE_PATTERN;
2771
+ const cleaned = String(options.scope).replace(/^\/+|\/+$/g, "");
2772
+ if (!cleaned)
2773
+ throw new Error("[storage/scoped] scope is required");
2774
+ if (!pattern.test(cleaned))
2775
+ throw new Error(`[storage/scoped] scope '${cleaned}' contains disallowed characters`);
2776
+ if (cleaned.includes("..") || cleaned.includes("/"))
2777
+ throw new Error(`[storage/scoped] scope '${cleaned}' cannot contain path separators or traversal`);
2778
+ this.inner = inner;
2779
+ this.scope = cleaned;
2780
+ this.scopeWithSlash = `${cleaned}/`;
2781
+ }
2782
+ scopePath(path2) {
2783
+ if (typeof path2 !== "string")
2784
+ throw new Error("[storage/scoped] path must be a string");
2785
+ if (path2.length === 0)
2786
+ return this.scope;
2787
+ if (path2.includes("\x00"))
2788
+ throw new Error("[storage/scoped] path contains a null byte");
2789
+ if (path2.startsWith("/") || /^[A-Za-z]:[\\/]/.test(path2))
2790
+ throw new Error(`[storage/scoped] path '${path2}' is absolute \u2014 refusing to escape scope`);
2791
+ const segments = path2.split(/[/\\]/);
2792
+ if (segments.some((s) => s === ".."))
2793
+ throw new Error(`[storage/scoped] path '${path2}' contains a '..' segment \u2014 refusing to escape scope`);
2794
+ return `${this.scope}/${path2.replace(/^\/+/, "")}`;
2795
+ }
2796
+ unscopePath(path2) {
2797
+ if (path2 === this.scope)
2798
+ return "";
2799
+ if (path2.startsWith(this.scopeWithSlash))
2800
+ return path2.slice(this.scopeWithSlash.length);
2801
+ return path2;
2802
+ }
2803
+ async write(path2, contents) {
2804
+ const result = await this.inner.write(this.scopePath(path2), contents);
2805
+ return { ...result, path: this.unscopePath(result.path) };
2806
+ }
2807
+ async read(path2) {
2808
+ return this.inner.read(this.scopePath(path2));
2809
+ }
2810
+ async readToString(path2) {
2811
+ return this.inner.readToString(this.scopePath(path2));
2812
+ }
2813
+ async readToBuffer(path2) {
2814
+ return this.inner.readToBuffer(this.scopePath(path2));
2815
+ }
2816
+ async readToUint8Array(path2) {
2817
+ return this.inner.readToUint8Array(this.scopePath(path2));
2818
+ }
2819
+ async deleteFile(path2) {
2820
+ return this.inner.deleteFile(this.scopePath(path2));
2821
+ }
2822
+ async deleteDirectory(path2) {
2823
+ return this.inner.deleteDirectory(this.scopePath(path2));
2824
+ }
2825
+ async createDirectory(path2) {
2826
+ return this.inner.createDirectory(this.scopePath(path2));
2827
+ }
2828
+ async moveFile(from, to) {
2829
+ return this.inner.moveFile(this.scopePath(from), this.scopePath(to));
2830
+ }
2831
+ async copyFile(from, to) {
2832
+ return this.inner.copyFile(this.scopePath(from), this.scopePath(to));
2833
+ }
2834
+ async stat(path2) {
2835
+ const entry = await this.inner.stat(this.scopePath(path2));
2836
+ return { ...entry, path: this.unscopePath(entry.path) };
2837
+ }
2838
+ list(path2, options) {
2839
+ const inner = this.inner.list(this.scopePath(path2), options);
2840
+ const unscope = this.unscopePath.bind(this);
2841
+ return async function* () {
2842
+ for await (const entry of inner) {
2843
+ yield { ...entry, path: unscope(entry.path) };
2844
+ }
2845
+ }();
2846
+ }
2847
+ async changeVisibility(path2, visibility2) {
2848
+ return this.inner.changeVisibility(this.scopePath(path2), visibility2);
2849
+ }
2850
+ async visibility(path2) {
2851
+ return this.inner.visibility(this.scopePath(path2));
2852
+ }
2853
+ async fileExists(path2) {
2854
+ return this.inner.fileExists(this.scopePath(path2));
2855
+ }
2856
+ async directoryExists(path2) {
2857
+ return this.inner.directoryExists(this.scopePath(path2));
2858
+ }
2859
+ async publicUrl(path2, options) {
2860
+ return this.inner.publicUrl(this.scopePath(path2), options);
2861
+ }
2862
+ async temporaryUrl(path2, options) {
2863
+ return this.inner.temporaryUrl(this.scopePath(path2), options);
2864
+ }
2865
+ async signedUrl(path2, options) {
2866
+ if (typeof this.inner.signedUrl !== "function")
2867
+ throw new Error("[storage/scoped] wrapped adapter does not support signedUrl");
2868
+ return this.inner.signedUrl(this.scopePath(path2), options);
2869
+ }
2870
+ async presignedUploadUrl(options) {
2871
+ if (typeof this.inner.presignedUploadUrl !== "function")
2872
+ throw new Error("[storage/scoped] wrapped adapter does not support presignedUploadUrl");
2873
+ const scopedDir = options.dir ? `${this.scope}/${options.dir.replace(/^\/+/, "")}` : this.scope;
2874
+ const result = await this.inner.presignedUploadUrl({ ...options, dir: scopedDir });
2875
+ return { ...result, path: this.unscopePath(result.path), key: this.unscopePath(result.key) };
2876
+ }
2877
+ async presignedUploadPolicy(options) {
2878
+ if (typeof this.inner.presignedUploadPolicy !== "function")
2879
+ throw new Error("[storage/scoped] wrapped adapter does not support presignedUploadPolicy");
2880
+ const scopedKey = typeof options.key === "string" ? this.scopePath(options.key) : { startsWith: this.scopePath(options.key.startsWith) };
2881
+ const result = await this.inner.presignedUploadPolicy({ ...options, key: scopedKey });
2882
+ return { ...result, key: this.unscopePath(result.key) };
2883
+ }
2884
+ async getStream(path2, options) {
2885
+ if (typeof this.inner.getStream !== "function")
2886
+ throw new Error("[storage/scoped] wrapped adapter does not support getStream");
2887
+ return this.inner.getStream(this.scopePath(path2), options);
2888
+ }
2889
+ async putStream(path2, stream, options) {
2890
+ if (typeof this.inner.putStream !== "function")
2891
+ throw new Error("[storage/scoped] wrapped adapter does not support putStream");
2892
+ const result = await this.inner.putStream(this.scopePath(path2), stream, options);
2893
+ return { ...result, path: this.unscopePath(result.path) };
2894
+ }
2895
+ async checksum(path2, options) {
2896
+ return this.inner.checksum(this.scopePath(path2), options);
2897
+ }
2898
+ async mimeType(path2, options) {
2899
+ return this.inner.mimeType(this.scopePath(path2), options);
2900
+ }
2901
+ async lastModified(path2) {
2902
+ return this.inner.lastModified(this.scopePath(path2));
2903
+ }
2904
+ async fileSize(path2) {
2905
+ return this.inner.fileSize(this.scopePath(path2));
2906
+ }
2907
+ }
2908
+ function scoped(inner, options) {
2909
+ return new ScopedStorageAdapter(inner, options);
2910
+ }
2911
+ // src/index.ts
2912
+ init_types();
2913
+
1694
2914
  // src/drivers/aws.ts
1695
- import { S3Client } from "@stacksjs/ts-cloud";
2915
+ init_s3();
1696
2916
  var _adapterPromise = null;
1697
2917
  async function loadConfig() {
1698
2918
  try {
1699
2919
  const { filesystems } = await import("@stacksjs/config");
1700
2920
  const s3Config = filesystems.s3;
1701
- const client = new S3Client(s3Config?.region || "us-east-1");
1702
- return createS3Storage(client, {
2921
+ return createS3Storage(null, {
1703
2922
  bucket: s3Config?.bucket || "stacks",
1704
2923
  prefix: s3Config?.prefix || "stx",
1705
2924
  region: s3Config?.region || "us-east-1"
1706
2925
  });
1707
2926
  } catch {
1708
2927
  const { env } = await import("@stacksjs/env");
1709
- const client = new S3Client(env.AWS_REGION || "us-east-1");
1710
- return createS3Storage(client, {
2928
+ return createS3Storage(null, {
1711
2929
  bucket: env.AWS_S3_BUCKET || "stacks",
1712
2930
  prefix: env.AWS_S3_PREFIX || "stx",
1713
2931
  region: env.AWS_REGION || "us-east-1"
@@ -1812,16 +3030,17 @@ var aws = {
1812
3030
  }
1813
3031
  };
1814
3032
  // src/drivers/local.ts
3033
+ init_local();
1815
3034
  import { resolve } from "path";
1816
- import process3 from "process";
3035
+ import process4 from "process";
1817
3036
  var _adapterPromise2 = null;
1818
3037
  async function loadConfig2() {
1819
3038
  try {
1820
3039
  const { filesystems } = await import("@stacksjs/config");
1821
- const rootDirectory = resolve(filesystems.root || process3.cwd());
3040
+ const rootDirectory = resolve(filesystems.root || process4.cwd());
1822
3041
  return createLocalStorage({ root: rootDirectory });
1823
3042
  } catch {
1824
- const rootDirectory = resolve(process3.cwd());
3043
+ const rootDirectory = resolve(process4.cwd());
1825
3044
  return createLocalStorage({ root: rootDirectory });
1826
3045
  }
1827
3046
  }
@@ -1992,15 +3211,15 @@ var memory2 = {
1992
3211
  };
1993
3212
  // src/drivers/bun.ts
1994
3213
  import { resolve as resolve2 } from "path";
1995
- import process4 from "process";
3214
+ import process5 from "process";
1996
3215
  var _adapterPromise3 = null;
1997
3216
  async function loadConfig3() {
1998
3217
  try {
1999
3218
  const { filesystems } = await import("@stacksjs/config");
2000
- const rootDirectory = resolve2(filesystems.root || process4.cwd());
3219
+ const rootDirectory = resolve2(filesystems.root || process5.cwd());
2001
3220
  return createBunStorage({ root: rootDirectory });
2002
3221
  } catch {
2003
- const rootDirectory = resolve2(process4.cwd());
3222
+ const rootDirectory = resolve2(process5.cwd());
2004
3223
  return createBunStorage({ root: rootDirectory });
2005
3224
  }
2006
3225
  }
@@ -2224,198 +3443,14 @@ function guessMime(filePath) {
2224
3443
  return "application/octet-stream";
2225
3444
  }
2226
3445
  }
2227
- // src/facade.ts
2228
- import { resolve as resolve3 } from "path";
2229
- import process5 from "process";
2230
- import { filesystems, app as appConfig } from "@stacksjs/config";
2231
- import { S3Client as S3Client2 } from "@stacksjs/ts-cloud";
2232
- function buildConfig() {
2233
- const cwd = process5.cwd();
2234
- const rootDir = filesystems.root || cwd;
2235
- const s3Config = filesystems.s3;
2236
- const appUrl = appConfig?.url || "";
2237
- const config = {
2238
- default: filesystems.driver || "local",
2239
- disks: {
2240
- local: {
2241
- driver: "local",
2242
- root: resolve3(rootDir, "storage/app"),
2243
- visibility: filesystems.defaultVisibility || "private"
2244
- },
2245
- public: {
2246
- driver: "local",
2247
- root: resolve3(rootDir, "public"),
2248
- url: appUrl ? `${appUrl}/storage` : "/storage",
2249
- visibility: "public"
2250
- }
2251
- }
2252
- };
2253
- if (s3Config?.bucket) {
2254
- config.disks.s3 = {
2255
- driver: "s3",
2256
- bucket: s3Config.bucket,
2257
- region: s3Config.region || "us-east-1",
2258
- prefix: s3Config.prefix,
2259
- endpoint: s3Config.endpoint,
2260
- url: filesystems.publicUrl?.domain,
2261
- usePathStyleEndpoint: !!s3Config.endpoint,
2262
- visibility: filesystems.defaultVisibility || "private",
2263
- credentials: s3Config.credentials ? { key: s3Config.credentials.accessKeyId, secret: s3Config.credentials.secretAccessKey } : undefined
2264
- };
2265
- }
2266
- return config;
2267
- }
2268
3446
 
2269
- class StorageManager {
2270
- _config = null;
2271
- disks = new Map;
2272
- s3Clients = new Map;
2273
- customConfig = null;
2274
- get config() {
2275
- if (!this._config) {
2276
- const builtConfig = buildConfig();
2277
- this._config = this.customConfig ? {
2278
- default: this.customConfig.default || builtConfig.default,
2279
- disks: { ...builtConfig.disks, ...this.customConfig.disks }
2280
- } : builtConfig;
2281
- }
2282
- return this._config;
2283
- }
2284
- init(config) {
2285
- this.customConfig = config;
2286
- this._config = null;
2287
- this.disks.clear();
2288
- this.s3Clients.clear();
2289
- return this;
2290
- }
2291
- disk(name) {
2292
- const diskName = name || this.config.default;
2293
- if (this.disks.has(diskName)) {
2294
- return this.disks.get(diskName);
2295
- }
2296
- const diskConfig = this.config.disks[diskName];
2297
- if (!diskConfig) {
2298
- const available = Object.keys(this.config.disks).join(", ");
2299
- throw new Error(`Disk [${diskName}] is not configured. Available: ${available}`);
2300
- }
2301
- const adapter2 = this.createAdapter(diskName, diskConfig);
2302
- this.disks.set(diskName, adapter2);
2303
- return adapter2;
2304
- }
2305
- createAdapter(name, config) {
2306
- switch (config.driver) {
2307
- case "local":
2308
- return this.createLocalAdapter(config);
2309
- case "s3":
2310
- return this.createS3Adapter(name, config);
2311
- default:
2312
- throw new Error(`Unsupported driver: ${config.driver}`);
2313
- }
2314
- }
2315
- createLocalAdapter(config) {
2316
- return createLocalStorage({ root: config.root });
2317
- }
2318
- createS3Adapter(name, config) {
2319
- let client = this.s3Clients.get(name);
2320
- if (!client) {
2321
- client = new S3Client2(config.region || "us-east-1");
2322
- this.s3Clients.set(name, client);
2323
- }
2324
- return new S3StorageAdapter(client, {
2325
- bucket: config.bucket,
2326
- region: config.region,
2327
- prefix: config.prefix
2328
- });
2329
- }
2330
- async put(path2, contents) {
2331
- return this.disk().write(path2, contents);
2332
- }
2333
- async get(path2) {
2334
- return this.disk().readToString(path2);
2335
- }
2336
- async exists(path2) {
2337
- return this.disk().fileExists(path2);
2338
- }
2339
- async missing(path2) {
2340
- return !await this.exists(path2);
2341
- }
2342
- async delete(path2) {
2343
- return this.disk().deleteFile(path2);
2344
- }
2345
- async copy(from, to) {
2346
- return this.disk().copyFile(from, to);
2347
- }
2348
- async move(from, to) {
2349
- return this.disk().moveFile(from, to);
2350
- }
2351
- async url(path2) {
2352
- return this.disk().publicUrl(path2);
2353
- }
2354
- async signedUrl(path2, options) {
2355
- const adapter2 = this.disk();
2356
- if (typeof adapter2.signedUrl !== "function") {
2357
- throw new Error(`[storage] disk '${this.config.default}' does not support signedUrl`);
2358
- }
2359
- return adapter2.signedUrl(path2, options);
2360
- }
2361
- async size(path2) {
2362
- return this.disk().fileSize(path2);
2363
- }
2364
- async lastModified(path2) {
2365
- return this.disk().lastModified(path2);
2366
- }
2367
- async mimeType(path2) {
2368
- return this.disk().mimeType(path2);
2369
- }
2370
- async checksum(path2, algorithm) {
2371
- return this.disk().checksum(path2, { algorithm });
2372
- }
2373
- async makeDirectory(path2) {
2374
- return this.disk().createDirectory(path2);
2375
- }
2376
- async deleteDirectory(path2) {
2377
- return this.disk().deleteDirectory(path2);
2378
- }
2379
- files(path2 = "") {
2380
- return this.disk().list(path2);
2381
- }
2382
- allFiles(path2 = "") {
2383
- return this.disk().list(path2, { deep: true });
2384
- }
2385
- configure(name, config) {
2386
- const currentConfig = this.config;
2387
- currentConfig.disks[name] = config;
2388
- this.disks.delete(name);
2389
- this.s3Clients.delete(name);
2390
- return this;
2391
- }
2392
- setDefaultDisk(name) {
2393
- if (!this.config.disks[name]) {
2394
- throw new Error(`Disk [${name}] is not configured`);
2395
- }
2396
- this.config.default = name;
2397
- return this;
2398
- }
2399
- getDiskConfig(name) {
2400
- return this.config.disks[name || this.config.default];
2401
- }
2402
- getConfiguredDisks() {
2403
- return Object.keys(this.config.disks);
2404
- }
2405
- getDefaultDisk() {
2406
- return this.config.default;
2407
- }
2408
- reset() {
2409
- this._config = null;
2410
- this.customConfig = null;
2411
- this.disks.clear();
2412
- this.s3Clients.clear();
2413
- return this;
2414
- }
2415
- }
2416
- var Storage = new StorageManager;
3447
+ // src/index.ts
3448
+ init_facade();
3449
+
2417
3450
  // src/uploaded-file.ts
3451
+ init_facade();
2418
3452
  import { basename as basename5, extname as extname2, join as join7 } from "path";
3453
+
2419
3454
  class UploadedFile {
2420
3455
  _file;
2421
3456
  _hashName = null;
@@ -2574,6 +3609,67 @@ function configFromEnv(base = {}) {
2574
3609
  }
2575
3610
  };
2576
3611
  }
3612
+
3613
+ // src/index.ts
3614
+ init_signed_url();
3615
+ init_path_sanitize();
3616
+
3617
+ // src/mime-verify.ts
3618
+ function detectMimeFromMagicBytes(bytes) {
3619
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
3620
+ if (view.length < 4)
3621
+ return null;
3622
+ if (view[0] === 137 && view[1] === 80 && view[2] === 78 && view[3] === 71 && view[4] === 13 && view[5] === 10 && view[6] === 26 && view[7] === 10)
3623
+ return "image/png";
3624
+ if (view[0] === 255 && view[1] === 216 && view[2] === 255)
3625
+ return "image/jpeg";
3626
+ if (view[0] === 71 && view[1] === 73 && view[2] === 70 && view[3] === 56)
3627
+ return "image/gif";
3628
+ if (view[0] === 82 && view[1] === 73 && view[2] === 70 && view[3] === 70 && view.length >= 12) {
3629
+ if (view[8] === 87 && view[9] === 69 && view[10] === 66 && view[11] === 80)
3630
+ return "image/webp";
3631
+ if (view[8] === 87 && view[9] === 65 && view[10] === 86 && view[11] === 69)
3632
+ return "audio/wav";
3633
+ }
3634
+ if (view[0] === 37 && view[1] === 80 && view[2] === 68 && view[3] === 70)
3635
+ return "application/pdf";
3636
+ if (view[0] === 80 && view[1] === 75 && (view[2] === 3 || view[2] === 5) && (view[3] === 4 || view[3] === 6))
3637
+ return "application/zip";
3638
+ if (view.length >= 12 && view[4] === 102 && view[5] === 116 && view[6] === 121 && view[7] === 112) {
3639
+ const brand = String.fromCharCode(view[8] ?? 0, view[9] ?? 0, view[10] ?? 0, view[11] ?? 0);
3640
+ if (brand === "avif" || brand === "avis")
3641
+ return "image/avif";
3642
+ if (brand === "heic" || brand === "heix" || brand === "mif1")
3643
+ return "image/heic";
3644
+ return "video/mp4";
3645
+ }
3646
+ if (view[0] === 26 && view[1] === 69 && view[2] === 223 && view[3] === 163)
3647
+ return "video/webm";
3648
+ if (view[0] === 73 && view[1] === 68 && view[2] === 51)
3649
+ return "audio/mpeg";
3650
+ if (view[0] === 255 && (view[1] ?? 0) >= 224)
3651
+ return "audio/mpeg";
3652
+ return null;
3653
+ }
3654
+ function normalizeContentType(contentType) {
3655
+ return contentType.split(";")[0]?.trim().toLowerCase() ?? "";
3656
+ }
3657
+ async function verifyUploadedMime(path2, expectedContentType, options = {}) {
3658
+ const { Storage: Storage2 } = await Promise.resolve().then(() => (init_facade(), exports_facade));
3659
+ const disk = Storage2.disk(options.disk);
3660
+ const bytes = await disk.readToUint8Array(path2);
3661
+ const expected = normalizeContentType(expectedContentType);
3662
+ const detected = detectMimeFromMagicBytes(bytes.slice(0, 32));
3663
+ const matches = detected === expected || detected === "image/jpeg" && (expected === "image/jpg" || expected === "image/pjpeg");
3664
+ return {
3665
+ ok: matches,
3666
+ expected,
3667
+ detected
3668
+ };
3669
+ }
3670
+
3671
+ // src/index.ts
3672
+ init_s3_presigned_post();
2577
3673
  export {
2578
3674
  zip,
2579
3675
  writeTextFile,
@@ -2581,6 +3677,7 @@ export {
2581
3677
  writeFileSync,
2582
3678
  writeFile,
2583
3679
  watchFile,
3680
+ verifyUploadedMime,
2584
3681
  verifySignedStorageToken,
2585
3682
  uploadedFiles,
2586
3683
  uploadedFile,
@@ -2588,13 +3685,19 @@ export {
2588
3685
  unzip,
2589
3686
  unarchive,
2590
3687
  exports_storage as storage,
3688
+ signS3PresignedPost,
2591
3689
  serveFile,
3690
+ scoped,
3691
+ sanitizePresignedFilename,
3692
+ sanitizePresignedDir,
2592
3693
  s3Disk,
3694
+ revokeSignedStorageToken,
2593
3695
  readTextFile,
2594
3696
  readPackageJson,
2595
3697
  readJsonFile,
2596
3698
  readFileSync,
2597
3699
  put,
3700
+ parseDiskPath,
2598
3701
  normalizeExpiryToMilliseconds,
2599
3702
  normalizeExpiryToDate,
2600
3703
  mkdirSync,
@@ -2602,6 +3705,7 @@ export {
2602
3705
  memory2 as memory,
2603
3706
  localDisk,
2604
3707
  local2 as local,
3708
+ isSignedStorageTokenRevoked,
2605
3709
  isFolder,
2606
3710
  isFile2 as isFile,
2607
3711
  isDirectoryEmpty,
@@ -2635,6 +3739,7 @@ export {
2635
3739
  doesNotExist,
2636
3740
  doesFolderExist,
2637
3741
  doesExist,
3742
+ detectMimeFromMagicBytes,
2638
3743
  deleteGlob,
2639
3744
  deleteFolder,
2640
3745
  deleteFiles,
@@ -2656,6 +3761,7 @@ export {
2656
3761
  copy,
2657
3762
  configFromEnv,
2658
3763
  compress,
3764
+ clearRevokedSignedStorageTokens,
2659
3765
  bun2 as bun,
2660
3766
  aws,
2661
3767
  archive,
@@ -2664,7 +3770,9 @@ export {
2664
3770
  UploadedFile,
2665
3771
  StorageManager,
2666
3772
  Storage,
3773
+ ScopedStorageAdapter,
2667
3774
  S3StorageAdapter,
3775
+ PathSanitizeError,
2668
3776
  LocalStorageAdapter,
2669
3777
  InMemoryStorageAdapter,
2670
3778
  BunStorageAdapter