@muhgholy/next-drive 4.23.8 → 4.23.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +152 -1
  2. package/dist/{chunk-OU5TKLHV.cjs → chunk-V75PCJHT.cjs} +116 -24
  3. package/dist/chunk-V75PCJHT.cjs.map +1 -0
  4. package/dist/{chunk-RBSFEEJJ.js → chunk-XUPDNN2U.js} +115 -25
  5. package/dist/chunk-XUPDNN2U.js.map +1 -0
  6. package/dist/client/file-chooser.d.ts +1 -0
  7. package/dist/client/file-chooser.d.ts.map +1 -1
  8. package/dist/client/hooks/use-upload.d.ts +1 -1
  9. package/dist/client/hooks/use-upload.d.ts.map +1 -1
  10. package/dist/client/index.cjs +88 -42
  11. package/dist/client/index.cjs.map +1 -1
  12. package/dist/client/index.js +87 -41
  13. package/dist/client/index.js.map +1 -1
  14. package/dist/server/actions/drive.d.ts.map +1 -1
  15. package/dist/server/config.d.ts.map +1 -1
  16. package/dist/server/controllers/drive.d.ts +26 -0
  17. package/dist/server/controllers/drive.d.ts.map +1 -1
  18. package/dist/server/database/mongoose/schema/drive.d.ts +1 -0
  19. package/dist/server/database/mongoose/schema/drive.d.ts.map +1 -1
  20. package/dist/server/express.cjs +11 -11
  21. package/dist/server/express.js +2 -2
  22. package/dist/server/hono.cjs +11 -11
  23. package/dist/server/hono.js +2 -2
  24. package/dist/server/index.cjs +24 -16
  25. package/dist/server/index.d.ts +1 -1
  26. package/dist/server/index.d.ts.map +1 -1
  27. package/dist/server/index.js +1 -1
  28. package/dist/server/zod/schemas.d.ts +5 -0
  29. package/dist/server/zod/schemas.d.ts.map +1 -1
  30. package/dist/types/lib/database/drive.d.ts +1 -0
  31. package/dist/types/lib/database/drive.d.ts.map +1 -1
  32. package/dist/types/server/config.d.ts +17 -0
  33. package/dist/types/server/config.d.ts.map +1 -1
  34. package/package.json +2 -1
  35. package/dist/chunk-OU5TKLHV.cjs.map +0 -1
  36. package/dist/chunk-RBSFEEJJ.js.map +0 -1
package/README.md CHANGED
@@ -7,7 +7,8 @@ File storage and management for Next.js and Express apps. Includes a responsive
7
7
  - 📁 **File Management** – Upload, rename, move, organize files and folders
8
8
  - 🔍 **Search** – Search active files or trash with real-time filtering
9
9
  - 🗑️ **Trash System** – Soft delete, restore, and empty trash
10
- - 📱 **Responsive UI** – Optimized for desktop and mobile
10
+ - **Anonymous Uploads** – One-time uploads with auto-expiry, confirmation, and abuse prevention
11
+ - �📱 **Responsive UI** – Optimized for desktop and mobile
11
12
  - 🎬 **Video Thumbnails** – Auto-generated thumbnails (requires FFmpeg)
12
13
  - 🔐 **Security** – Signed URLs and configurable upload limits
13
14
  - 📊 **View Modes** – Grid/List views with sorting and grouping
@@ -188,6 +189,8 @@ function MyForm() {
188
189
  }
189
190
  ```
190
191
 
192
+ > For unauthenticated, one-time uploads, add the `allowUnauthenticated` prop. See [Anonymous (One-Time) Uploads](#anonymous-one-time-uploads).
193
+
191
194
  ---
192
195
 
193
196
  ## Express Integration
@@ -582,6 +585,128 @@ await driveDelete(items[0]);
582
585
 
583
586
  ---
584
587
 
588
+ ## Anonymous (One-Time) Uploads
589
+
590
+ Let **unauthenticated** users upload a file once — without giving them access to a drive. The user picks a file, it uploads with progress (just like the drive explorer), and you receive a normal `TDriveFile` (with a real `drive.id`). The file is **temporary**: it is automatically deleted after a TTL (default **60 minutes**) unless your backend explicitly confirms it.
591
+
592
+ The same `DriveFileChooser` adapts to the visitor automatically: **logged-in users get the full drive explorer** (browse + pick existing files or upload to their own drive, stored permanently), while **anonymous visitors get a one-time upload** (temporary, auto-expiring). You opt in with a single `allowUnauthenticated` prop.
593
+
594
+ This is useful for forms where you need a `drive.id` for an anonymous visitor (e.g. a contact form attachment, a job application, or a guest submission) but don't want to expose your authenticated drive.
595
+
596
+ **Lifecycle:** `pick file → upload → returns TDriveFile (temporary)` → your backend saves `drive.id` → `driveConfirm(id)` keeps it → otherwise `drivePurgeExpired()` deletes it after the TTL.
597
+
598
+ ### 1. Enable in your server configuration
599
+
600
+ Add an `unauthenticated` block to `security`. It is **completely separate** from your authenticated limits (it has its own size and MIME allow-list). The feature is disabled unless `enabled: true`.
601
+
602
+ ```typescript
603
+ security: {
604
+ // ...your normal authenticated limits
605
+ maxUploadSizeInBytes: 50 * 1024 * 1024,
606
+ allowedMimeTypes: ["image/*", "video/*", "application/pdf"],
607
+
608
+ unauthenticated: {
609
+ enabled: true,
610
+ ttlMinutes: 60, // Lifetime before auto-delete (default 60)
611
+ maxUploadSizeInBytes: 25 * 1024 * 1024, // 25MB (separate from authenticated limit)
612
+ allowedMimeTypes: ["image/*", "application/pdf"],
613
+
614
+ // Optional abuse prevention (all fields optional)
615
+ abuse: {
616
+ perIp: { windowMinutes: 10, max: 20 }, // Max 20 uploads / 10 min per IP
617
+ hourlyPerIp: 60, // Max 60 uploads / hour per IP
618
+ maxConcurrent: 10, // Max simultaneous anonymous uploads (global)
619
+ maxLiveBytes: 2 * 1024 * 1024 * 1024, // Max total live temporary storage (global)
620
+ },
621
+ },
622
+ }
623
+ ```
624
+
625
+ > Anonymous uploads do **not** count against any user's quota and are stored with no owner.
626
+
627
+ ### 2. Client component
628
+
629
+ Pass the `allowUnauthenticated` prop to `DriveFileChooser`. It checks the visitor's auth status once on mount:
630
+
631
+ - **Logged in** → opens the normal drive explorer (browse and pick existing files or upload to their own drive).
632
+ - **Not logged in** → opens the native file picker and uploads anonymously, returning the uploaded `TDriveFile`.
633
+
634
+ ```tsx
635
+ import { useState } from "react";
636
+ import { DriveFileChooser } from "@muhgholy/next-drive/client";
637
+ import type { TDriveFile } from "@muhgholy/next-drive/client";
638
+
639
+ function GuestForm() {
640
+ const [file, setFile] = useState<TDriveFile | null>(null);
641
+
642
+ return (
643
+ <DriveFileChooser
644
+ allowUnauthenticated
645
+ value={file}
646
+ onChange={setFile}
647
+ accept="image/*"
648
+ placeholder="Upload an attachment"
649
+ />
650
+ );
651
+ }
652
+ ```
653
+
654
+ > `DriveFileChooser` must be inside a `DriveProvider` (it reads `apiEndpoint` and resolves auth from context). The `multiple` and `accept` props work the same as the standard chooser.
655
+
656
+ ### 3. Confirm to keep the file
657
+
658
+ After you persist the returned `drive.id` (e.g. attach it to a submitted form record), call `driveConfirm` to clear its expiry so it is never auto-deleted:
659
+
660
+ ```typescript
661
+ import { driveConfirm } from "@muhgholy/next-drive/server";
662
+
663
+ const kept = await driveConfirm(driveFile.id);
664
+ // kept === true if the file exists
665
+ ```
666
+
667
+ **Returns:** `Promise<boolean>` — `true` if the file exists. Safe to call on files uploaded while logged in (they are already permanent, so it is a harmless no-op) — so your form handler can always call `driveConfirm` regardless of whether the visitor was authenticated.
668
+
669
+ ### 4. Purge expired (unconfirmed) files
670
+
671
+ Run `drivePurgeExpired` on a schedule (cron job or interval) to permanently delete temporary files whose TTL has passed and were never confirmed:
672
+
673
+ ```typescript
674
+ import { drivePurgeExpired } from "@muhgholy/next-drive/server";
675
+
676
+ // e.g. in a cron route or a setInterval on your server
677
+ const result = await drivePurgeExpired();
678
+ console.log(`Purged ${result.removed.length} expired files, freed ${result.totalFreedInBytes} bytes`);
679
+ ```
680
+
681
+ **Returns:** `{ removed: string[], totalFreedInBytes: number }`
682
+
683
+ > [!IMPORTANT]
684
+ > Confirmed files are never purged. Only files that are both expired **and** unconfirmed are removed. If you never call `drivePurgeExpired`, temporary files will remain — schedule it (e.g. hourly).
685
+
686
+ ### Determining the client IP (behind a proxy)
687
+
688
+ Per-IP limits resolve the client IP from `cf-connecting-ip`, then the first entry of `x-forwarded-for`, then the socket address. Override the trusted headers or supply your own resolver:
689
+
690
+ ```typescript
691
+ unauthenticated: {
692
+ enabled: true,
693
+ maxUploadSizeInBytes: 25 * 1024 * 1024,
694
+ allowedMimeTypes: ["image/*"],
695
+ abuse: {
696
+ perIp: { windowMinutes: 10, max: 20 },
697
+ trustedHeaders: ["cf-connecting-ip", "x-forwarded-for"], // checked in order
698
+ clientId: (req) => (req.headers["cf-connecting-ip"] as string) ?? "unknown", // full override
699
+ },
700
+ }
701
+ ```
702
+
703
+ > [!WARNING]
704
+ > Only trust forwarded IP headers if every request reaches your origin through a proxy you control (e.g. Cloudflare). If clients can reach the origin directly, these headers can be spoofed — lock your origin down to your proxy's IPs.
705
+ >
706
+ > Abuse counters are kept in-memory per process. In a multi-instance deployment, each instance enforces limits independently; use a shared store (e.g. Redis) if you need cross-instance limits.
707
+
708
+ ---
709
+
585
710
  ## Configuration Options
586
711
 
587
712
  ### Security
@@ -596,9 +721,35 @@ security: {
596
721
  expiresIn: 3600, // seconds
597
722
  },
598
723
  trash: { retentionDays: 30 },
724
+ // Anonymous one-time uploads (see "Anonymous (One-Time) Uploads" section)
725
+ unauthenticated: {
726
+ enabled: true,
727
+ ttlMinutes: 60,
728
+ maxUploadSizeInBytes: 25 * 1024 * 1024,
729
+ allowedMimeTypes: ['image/*', 'application/pdf'],
730
+ abuse: {
731
+ perIp: { windowMinutes: 10, max: 20 },
732
+ hourlyPerIp: 60,
733
+ maxConcurrent: 10,
734
+ maxLiveBytes: 2 * 1024 * 1024 * 1024,
735
+ },
736
+ },
599
737
  }
600
738
  ```
601
739
 
740
+ | Option | Type | Default | Description |
741
+ | ------------------------------------- | ---------- | ----------- | ---------------------------------------------------------------- |
742
+ | `unauthenticated.enabled` | `boolean` | `false` | Enable anonymous one-time uploads |
743
+ | `unauthenticated.ttlMinutes` | `number` | `60` | Minutes before an unconfirmed upload becomes eligible for purge |
744
+ | `unauthenticated.maxUploadSizeInBytes`| `number` | — | Max size per anonymous upload (separate from authenticated) |
745
+ | `unauthenticated.allowedMimeTypes` | `string[]` | — | Allowed MIME types for anonymous uploads |
746
+ | `unauthenticated.abuse.perIp` | `{ windowMinutes, max }` | — | Sliding-window per-IP upload limit |
747
+ | `unauthenticated.abuse.hourlyPerIp` | `number` | — | Max anonymous uploads per IP per hour |
748
+ | `unauthenticated.abuse.maxConcurrent` | `number` | — | Max simultaneous anonymous uploads (global, per process) |
749
+ | `unauthenticated.abuse.maxLiveBytes` | `number` | — | Max total live temporary storage in bytes (global) |
750
+ | `unauthenticated.abuse.trustedHeaders`| `string[]` | `['cf-connecting-ip', 'x-forwarded-for']` | Headers checked in order to resolve client IP |
751
+ | `unauthenticated.abuse.clientId` | `(req) => string` | — | Full override for resolving the client identifier |
752
+
602
753
  ### CORS (Cross-Origin)
603
754
 
604
755
  Required when client and API are on different domains:
@@ -49,6 +49,7 @@ var DriveSchema = new mongoose.Schema(
49
49
  information: { type: informationSchema, required: true },
50
50
  status: { type: String, enum: ["READY", "PROCESSING", "UPLOADING", "FAILED"], default: "PROCESSING" },
51
51
  trashedAt: { type: Date, default: null },
52
+ expiresAt: { type: Date, default: null },
52
53
  meta: { type: mongoose.Schema.Types.Mixed, default: {} },
53
54
  createdAt: { type: Date, default: Date.now }
54
55
  },
@@ -61,6 +62,7 @@ DriveSchema.index({ owner: 1, trashedAt: 1 });
61
62
  DriveSchema.index({ owner: 1, "information.hash": 1 });
62
63
  DriveSchema.index({ owner: 1, name: "text" });
63
64
  DriveSchema.index({ owner: 1, "provider.type": 1 });
65
+ DriveSchema.index({ expiresAt: 1 });
64
66
  DriveSchema.method("toClient", async function() {
65
67
  const data = this.toJSON();
66
68
  return {
@@ -73,6 +75,7 @@ DriveSchema.method("toClient", async function() {
73
75
  information: data.information,
74
76
  status: data.status,
75
77
  trashedAt: data.trashedAt,
78
+ expiresAt: data.expiresAt,
76
79
  meta: data.meta,
77
80
  createdAt: data.createdAt
78
81
  };
@@ -259,7 +262,8 @@ var getGlobal = () => {
259
262
  globalThis[GLOBAL_KEY] = {
260
263
  config: null,
261
264
  migrationPromise: null,
262
- initialized: false
265
+ initialized: false,
266
+ abuse: { ipHits: /* @__PURE__ */ new Map(), concurrent: 0 }
263
267
  };
264
268
  }
265
269
  return globalThis[GLOBAL_KEY];
@@ -288,7 +292,8 @@ var driveConfiguration = async (config) => {
288
292
  // 10GB default for ROOT
289
293
  allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
290
294
  signedUrls: config.security?.signedUrls,
291
- trash: config.security?.trash
295
+ trash: config.security?.trash,
296
+ unauthenticated: config.security?.unauthenticated
292
297
  }
293
298
  };
294
299
  } else {
@@ -306,7 +311,8 @@ var driveConfiguration = async (config) => {
306
311
  maxUploadSizeInBytes: config.security?.maxUploadSizeInBytes ?? 10 * 1024 * 1024,
307
312
  allowedMimeTypes: config.security?.allowedMimeTypes ?? ["*/*"],
308
313
  signedUrls: config.security?.signedUrls,
309
- trash: config.security?.trash
314
+ trash: config.security?.trash,
315
+ unauthenticated: config.security?.unauthenticated
310
316
  },
311
317
  information: config.information
312
318
  };
@@ -1540,7 +1546,8 @@ var uploadChunkSchema = zod.z.object({
1540
1546
  fileName: nameSchema,
1541
1547
  fileSize: zod.z.number().int().min(0).max(Number.MAX_SAFE_INTEGER),
1542
1548
  fileType: zod.z.string().min(1).max(255),
1543
- folderId: zod.z.string().optional()
1549
+ folderId: zod.z.string().optional(),
1550
+ unauthenticated: zod.z.coerce.boolean().optional()
1544
1551
  }).refine((data) => data.chunkIndex < data.totalChunks, {
1545
1552
  message: "Chunk index must be less than total chunks"
1546
1553
  });
@@ -2134,6 +2141,26 @@ var driveCleanup = async () => {
2134
2141
  }
2135
2142
  return { removed, totalFreedInBytes };
2136
2143
  };
2144
+ var driveConfirm = async (id) => {
2145
+ const result = await drive_default.updateOne({ _id: id }, { $set: { expiresAt: null } });
2146
+ return result.matchedCount > 0;
2147
+ };
2148
+ var drivePurgeExpired = async () => {
2149
+ const expired = await drive_default.find({ expiresAt: { $ne: null, $lt: /* @__PURE__ */ new Date() } });
2150
+ const removed = [];
2151
+ let totalFreedInBytes = 0;
2152
+ for (const drive of expired) {
2153
+ const id = String(drive._id);
2154
+ try {
2155
+ await driveDelete(drive);
2156
+ totalFreedInBytes += drive.information.type === "FILE" ? drive.information.sizeInBytes : 0;
2157
+ removed.push(id);
2158
+ } catch (e) {
2159
+ console.error(`[next-drive] Failed to purge expired file ${id}:`, e);
2160
+ }
2161
+ }
2162
+ return { removed, totalFreedInBytes };
2163
+ };
2137
2164
 
2138
2165
  // src/server/actions/shared.ts
2139
2166
  var resolveProvider = async (req, owner) => {
@@ -2248,36 +2275,83 @@ var handleDriveAction = async (ctx) => {
2248
2275
  cleanupTempFiles(files);
2249
2276
  return void res.status(400).json({ status: 400, message: uploadData.error.errors[0].message });
2250
2277
  }
2251
- const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId } = uploadData.data;
2278
+ const { chunkIndex, totalChunks, driveId, fileName, fileSize: fileSizeInBytes, fileType, folderId, unauthenticated } = uploadData.data;
2252
2279
  let currentUploadId = driveId;
2253
2280
  const tempBaseDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads");
2254
2281
  if (!currentUploadId) {
2255
2282
  if (chunkIndex !== 0) return void res.status(400).json({ message: "Could not upload: missing upload session for this chunk" });
2256
- if (fileType && config.security) {
2257
- if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
2283
+ if (unauthenticated) {
2284
+ const unauth = config.security?.unauthenticated;
2285
+ if (!unauth?.enabled) {
2258
2286
  cleanupTempFiles(files);
2259
- return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2287
+ return void res.status(403).json({ status: 403, message: "Anonymous uploads are not enabled" });
2260
2288
  }
2261
- }
2262
- if (!isRootMode) {
2263
- const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2264
- if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
2289
+ if (fileSizeInBytes > unauth.maxUploadSizeInBytes) {
2265
2290
  cleanupTempFiles(files);
2266
- return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
2291
+ return void res.status(413).json({ status: 413, message: "Could not upload: file exceeds the maximum allowed size" });
2292
+ }
2293
+ if (fileType && !validateMimeType(fileType, unauth.allowedMimeTypes)) {
2294
+ cleanupTempFiles(files);
2295
+ return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2296
+ }
2297
+ const abuse = unauth.abuse;
2298
+ if (abuse) {
2299
+ const store = globalThis.__nextDrive.abuse;
2300
+ const now = Date.now();
2301
+ const ip = abuse.clientId?.(req) ?? (abuse.trustedHeaders ?? ["cf-connecting-ip", "x-forwarded-for"]).map((h) => req.headers[h]).find(Boolean)?.split(",")[0].trim() ?? req.socket.remoteAddress ?? "unknown";
2302
+ const hits = (store.ipHits.get(ip) ?? []).filter((t) => now - t < 36e5);
2303
+ const perIp = abuse.perIp;
2304
+ if (perIp && hits.filter((t) => now - t < perIp.windowMinutes * 6e4).length >= perIp.max) {
2305
+ cleanupTempFiles(files);
2306
+ return void res.status(429).json({ status: 429, message: "Too many uploads, please try again later" });
2307
+ }
2308
+ if (abuse.hourlyPerIp && hits.length >= abuse.hourlyPerIp) {
2309
+ cleanupTempFiles(files);
2310
+ return void res.status(429).json({ status: 429, message: "Hourly upload limit reached, please try again later" });
2311
+ }
2312
+ if (abuse.maxConcurrent && store.concurrent >= abuse.maxConcurrent) {
2313
+ cleanupTempFiles(files);
2314
+ return void res.status(429).json({ status: 429, message: "Server is busy with uploads, please try again later" });
2315
+ }
2316
+ if (abuse.maxLiveBytes) {
2317
+ const [agg] = await drive_default.aggregate([{ $match: { expiresAt: { $ne: null } } }, { $group: { _id: null, total: { $sum: "$information.sizeInBytes" } } }]);
2318
+ if ((agg?.total ?? 0) + fileSizeInBytes > abuse.maxLiveBytes) {
2319
+ cleanupTempFiles(files);
2320
+ return void res.status(429).json({ status: 429, message: "Temporary storage is full, please try again later" });
2321
+ }
2322
+ }
2323
+ hits.push(now);
2324
+ store.ipHits.set(ip, hits);
2325
+ store.concurrent++;
2326
+ }
2327
+ } else {
2328
+ if (fileType && config.security) {
2329
+ if (!validateMimeType(fileType, config.security.allowedMimeTypes)) {
2330
+ cleanupTempFiles(files);
2331
+ return void res.status(400).json({ status: 400, message: `Could not upload: file type "${fileType}" is not allowed` });
2332
+ }
2333
+ }
2334
+ if (!isRootMode) {
2335
+ const quota = await provider.getQuota(owner, accountId, information.storage.quotaInBytes);
2336
+ if (quota.usedInBytes + fileSizeInBytes > quota.quotaInBytes) {
2337
+ cleanupTempFiles(files);
2338
+ return void res.status(413).json({ status: 413, message: "Could not upload: you have run out of storage space" });
2339
+ }
2267
2340
  }
2268
2341
  }
2269
2342
  currentUploadId = crypto3__default.default.randomUUID();
2270
2343
  const uploadDir2 = path__default.default.join(tempBaseDir, currentUploadId);
2271
2344
  fs__default.default.mkdirSync(uploadDir2, { recursive: true });
2272
2345
  const metadata = {
2273
- owner,
2274
- accountId,
2346
+ owner: unauthenticated ? null : owner,
2347
+ accountId: unauthenticated ? null : accountId,
2275
2348
  providerName: provider.name,
2276
2349
  name: fileName,
2277
- parentId: folderId === "root" || !folderId ? null : folderId,
2350
+ parentId: unauthenticated || folderId === "root" || !folderId ? null : folderId,
2278
2351
  fileSize: fileSizeInBytes,
2279
2352
  mimeType: fileType,
2280
- totalChunks
2353
+ totalChunks,
2354
+ unauthenticated: !!unauthenticated
2281
2355
  };
2282
2356
  fs__default.default.writeFileSync(path__default.default.join(uploadDir2, "metadata.json"), JSON.stringify(metadata));
2283
2357
  }
@@ -2363,7 +2437,8 @@ var handleDriveAction = async (ctx) => {
2363
2437
  information: { type: "FILE", sizeInBytes: meta.fileSize, mime: meta.mimeType, path: "" },
2364
2438
  status: "UPLOADING",
2365
2439
  currentChunk: totalChunks,
2366
- totalChunks
2440
+ totalChunks,
2441
+ expiresAt: meta.unauthenticated ? new Date(Date.now() + (config.security?.unauthenticated?.ttlMinutes ?? 60) * 6e4) : null
2367
2442
  });
2368
2443
  if (meta.providerName === "LOCAL" && drive.information.type === "FILE") {
2369
2444
  drive.information.path = path__default.default.join("file", String(drive._id), "data.bin");
@@ -2372,10 +2447,12 @@ var handleDriveAction = async (ctx) => {
2372
2447
  try {
2373
2448
  const item = await provider.uploadFile(drive, finalTempPath, meta.accountId);
2374
2449
  fs__default.default.rmSync(uploadDir, { recursive: true, force: true });
2450
+ if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2375
2451
  const newQuota = await provider.getQuota(meta.owner, meta.accountId, information.storage.quotaInBytes);
2376
2452
  res.status(200).json({ status: 200, message: "Upload complete", data: { type: "UPLOAD_COMPLETE", driveId: String(drive._id), item: withSignedUrl(item, config) }, statistic: { storage: newQuota } });
2377
2453
  } catch (err) {
2378
2454
  await drive_default.deleteOne({ _id: drive._id });
2455
+ if (meta.unauthenticated) globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2379
2456
  throw err;
2380
2457
  }
2381
2458
  } else {
@@ -2399,6 +2476,10 @@ var handleDriveAction = async (ctx) => {
2399
2476
  const tempUploadDir = path__default.default.join(os2__default.default.tmpdir(), "next-drive-uploads", id);
2400
2477
  if (fs__default.default.existsSync(tempUploadDir)) {
2401
2478
  try {
2479
+ const metaPath = path__default.default.join(tempUploadDir, "metadata.json");
2480
+ if (fs__default.default.existsSync(metaPath) && JSON.parse(fs__default.default.readFileSync(metaPath, "utf-8")).unauthenticated) {
2481
+ globalThis.__nextDrive.abuse.concurrent = Math.max(0, globalThis.__nextDrive.abuse.concurrent - 1);
2482
+ }
2402
2483
  fs__default.default.rmSync(tempUploadDir, { recursive: true, force: true });
2403
2484
  } catch (e) {
2404
2485
  console.error("Failed to cleanup temp upload:", e);
@@ -2605,12 +2686,16 @@ var driveAPIHandler = async (req, res) => {
2605
2686
  if (wasPublicHandled) return;
2606
2687
  try {
2607
2688
  const mode = config.mode || "NORMAL";
2608
- const information = await getDriveInformation({ method: "REQUEST", req });
2609
- const { key: owner } = information;
2610
- const isRootMode = mode === "ROOT";
2611
2689
  if (action === "information") {
2612
2690
  const { clientId, clientSecret, redirectUri } = config.storage?.google || {};
2613
2691
  const googleConfigured = !!(clientId && clientSecret && redirectUri);
2692
+ let authenticated = false;
2693
+ try {
2694
+ await getDriveInformation({ method: "REQUEST", req });
2695
+ authenticated = true;
2696
+ } catch {
2697
+ authenticated = false;
2698
+ }
2614
2699
  res.status(200).json({
2615
2700
  status: 200,
2616
2701
  message: "Information retrieved",
@@ -2618,11 +2703,16 @@ var driveAPIHandler = async (req, res) => {
2618
2703
  providers: {
2619
2704
  google: googleConfigured
2620
2705
  },
2621
- mode
2706
+ mode,
2707
+ authenticated,
2708
+ unauthenticatedUploads: !!config.security?.unauthenticated?.enabled
2622
2709
  }
2623
2710
  });
2624
2711
  return;
2625
2712
  }
2713
+ const information = await getDriveInformation({ method: "REQUEST", req });
2714
+ const { key: owner } = information;
2715
+ const isRootMode = mode === "ROOT";
2626
2716
  const wasAuthHandled = await handleAuthAction(req, res, action, config, owner);
2627
2717
  if (wasAuthHandled) return;
2628
2718
  const { provider, accountId } = await resolveProvider(req, owner);
@@ -2647,6 +2737,7 @@ var driveAPIHandler = async (req, res) => {
2647
2737
  exports.driveAPIHandler = driveAPIHandler;
2648
2738
  exports.driveCleanup = driveCleanup;
2649
2739
  exports.driveConfiguration = driveConfiguration;
2740
+ exports.driveConfirm = driveConfirm;
2650
2741
  exports.driveDelete = driveDelete;
2651
2742
  exports.driveFilePath = driveFilePath;
2652
2743
  exports.driveFileSchemaZod = driveFileSchemaZod;
@@ -2654,10 +2745,11 @@ exports.driveGetUrl = driveGetUrl;
2654
2745
  exports.driveInfo = driveInfo;
2655
2746
  exports.driveList = driveList;
2656
2747
  exports.driveListFiles = driveListFiles;
2748
+ exports.drivePurgeExpired = drivePurgeExpired;
2657
2749
  exports.driveReadFile = driveReadFile;
2658
2750
  exports.driveUpload = driveUpload;
2659
2751
  exports.drive_default = drive_default;
2660
2752
  exports.getDriveConfig = getDriveConfig;
2661
2753
  exports.getDriveInformation = getDriveInformation;
2662
- //# sourceMappingURL=chunk-OU5TKLHV.cjs.map
2663
- //# sourceMappingURL=chunk-OU5TKLHV.cjs.map
2754
+ //# sourceMappingURL=chunk-V75PCJHT.cjs.map
2755
+ //# sourceMappingURL=chunk-V75PCJHT.cjs.map