@muhgholy/next-drive 4.23.7 → 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 (95) hide show
  1. package/README.md +152 -1
  2. package/dist/{chunk-LAKT7IJJ.cjs → chunk-V75PCJHT.cjs} +962 -773
  3. package/dist/chunk-V75PCJHT.cjs.map +1 -0
  4. package/dist/{chunk-MVYNW56R.js → chunk-XUPDNN2U.js} +957 -770
  5. package/dist/chunk-XUPDNN2U.js.map +1 -0
  6. package/dist/client/components/drive/{RenameAccountDialog.d.ts → account/rename.d.ts} +2 -2
  7. package/dist/client/components/drive/account/rename.d.ts.map +1 -0
  8. package/dist/client/components/drive/{dnd-provider.d.ts → dnd/context.d.ts} +1 -1
  9. package/dist/client/components/drive/dnd/context.d.ts.map +1 -0
  10. package/dist/client/components/drive/{CreateFolderDialog.d.ts → folder/create.d.ts} +2 -2
  11. package/dist/client/components/drive/folder/create.d.ts.map +1 -0
  12. package/dist/client/components/drive/{RenameDialog.d.ts → item/rename.d.ts} +3 -3
  13. package/dist/client/components/drive/item/rename.d.ts.map +1 -0
  14. package/dist/client/components/{dialog.d.ts → shared/confirm.d.ts} +2 -2
  15. package/dist/client/components/shared/confirm.d.ts.map +1 -0
  16. package/dist/client/components/ui/{alert-dialog.d.ts → alert-modal.d.ts} +1 -1
  17. package/dist/client/components/ui/alert-modal.d.ts.map +1 -0
  18. package/dist/client/components/ui/{dialog-fullscreen.d.ts → fullscreen.d.ts} +1 -1
  19. package/dist/client/components/ui/fullscreen.d.ts.map +1 -0
  20. package/dist/client/components/ui/{dialog.d.ts → modal.d.ts} +1 -1
  21. package/dist/client/components/ui/modal.d.ts.map +1 -0
  22. package/dist/client/context.d.ts.map +1 -1
  23. package/dist/client/file-chooser.d.ts +1 -0
  24. package/dist/client/file-chooser.d.ts.map +1 -1
  25. package/dist/client/hooks/{useUpload.d.ts → use-upload.d.ts} +2 -2
  26. package/dist/client/hooks/use-upload.d.ts.map +1 -0
  27. package/dist/client/index.cjs +315 -206
  28. package/dist/client/index.cjs.map +1 -1
  29. package/dist/client/index.d.ts +12 -11
  30. package/dist/client/index.d.ts.map +1 -1
  31. package/dist/client/index.js +314 -205
  32. package/dist/client/index.js.map +1 -1
  33. package/dist/server/actions/auth.d.ts +4 -0
  34. package/dist/server/actions/auth.d.ts.map +1 -0
  35. package/dist/server/actions/cors.d.ts +4 -0
  36. package/dist/server/actions/cors.d.ts.map +1 -0
  37. package/dist/server/actions/drive.d.ts +18 -0
  38. package/dist/server/actions/drive.d.ts.map +1 -0
  39. package/dist/server/actions/public.d.ts +4 -0
  40. package/dist/server/actions/public.d.ts.map +1 -0
  41. package/dist/server/actions/shared.d.ts +14 -0
  42. package/dist/server/actions/shared.d.ts.map +1 -0
  43. package/dist/server/config.d.ts.map +1 -1
  44. package/dist/server/controllers/drive.d.ts +26 -0
  45. package/dist/server/controllers/drive.d.ts.map +1 -1
  46. package/dist/server/database/mongoose/schema/drive.d.ts +1 -0
  47. package/dist/server/database/mongoose/schema/drive.d.ts.map +1 -1
  48. package/dist/server/express.cjs +11 -11
  49. package/dist/server/express.js +2 -2
  50. package/dist/server/hono.cjs +11 -11
  51. package/dist/server/hono.js +2 -2
  52. package/dist/server/index.cjs +24 -16
  53. package/dist/server/index.d.ts +2 -2
  54. package/dist/server/index.d.ts.map +1 -1
  55. package/dist/server/index.js +1 -1
  56. package/dist/server/security/{cryptoUtils.d.ts → crypto-utils.d.ts} +1 -1
  57. package/dist/server/security/crypto-utils.d.ts.map +1 -0
  58. package/dist/server/security/{mimeFilter.d.ts → mime-filter.d.ts} +1 -1
  59. package/dist/server/security/mime-filter.d.ts.map +1 -0
  60. package/dist/server/storage-adapters/google.d.ts.map +1 -0
  61. package/dist/server/storage-adapters/local.d.ts.map +1 -0
  62. package/dist/server/utils/{folderValidation.d.ts → folder-validation.d.ts} +1 -1
  63. package/dist/server/utils/folder-validation.d.ts.map +1 -0
  64. package/dist/server/utils/{imageConvert.d.ts → image-convert.d.ts} +1 -1
  65. package/dist/server/utils/image-convert.d.ts.map +1 -0
  66. package/dist/server/zod/schemas.d.ts +5 -0
  67. package/dist/server/zod/schemas.d.ts.map +1 -1
  68. package/dist/types/lib/database/drive.d.ts +1 -0
  69. package/dist/types/lib/database/drive.d.ts.map +1 -1
  70. package/dist/types/lib/database/index.d.ts +2 -2
  71. package/dist/types/lib/database/index.d.ts.map +1 -1
  72. package/dist/types/server/config.d.ts +17 -0
  73. package/dist/types/server/config.d.ts.map +1 -1
  74. package/dist/types/server/index.d.ts +5 -5
  75. package/dist/types/server/index.d.ts.map +1 -1
  76. package/package.json +2 -1
  77. package/dist/chunk-LAKT7IJJ.cjs.map +0 -1
  78. package/dist/chunk-MVYNW56R.js.map +0 -1
  79. package/dist/client/components/dialog.d.ts.map +0 -1
  80. package/dist/client/components/drive/CreateFolderDialog.d.ts.map +0 -1
  81. package/dist/client/components/drive/RenameAccountDialog.d.ts.map +0 -1
  82. package/dist/client/components/drive/RenameDialog.d.ts.map +0 -1
  83. package/dist/client/components/drive/dnd-provider.d.ts.map +0 -1
  84. package/dist/client/components/ui/alert-dialog.d.ts.map +0 -1
  85. package/dist/client/components/ui/dialog-fullscreen.d.ts.map +0 -1
  86. package/dist/client/components/ui/dialog.d.ts.map +0 -1
  87. package/dist/client/hooks/useUpload.d.ts.map +0 -1
  88. package/dist/server/providers/google.d.ts.map +0 -1
  89. package/dist/server/providers/local.d.ts.map +0 -1
  90. package/dist/server/security/cryptoUtils.d.ts.map +0 -1
  91. package/dist/server/security/mimeFilter.d.ts.map +0 -1
  92. package/dist/server/utils/folderValidation.d.ts.map +0 -1
  93. package/dist/server/utils/imageConvert.d.ts.map +0 -1
  94. /package/dist/server/{providers → storage-adapters}/google.d.ts +0 -0
  95. /package/dist/server/{providers → storage-adapters}/local.d.ts +0 -0
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: