@thetechfossil/upfiles 0.5.0 → 1.0.0

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/README.md CHANGED
@@ -1,414 +1,443 @@
1
- # @thetechfossil/upfiles
2
-
3
- Lightweight JavaScript client and React component to upload files via a presigned S3 flow. Similar to UploadThing-like DX.
4
-
5
- - Client: get presigned URLs and PUT to S3
6
- - React `<Uploader />`: drag/drop, progress, single/multiple, accepts/types, size limits
7
- - React `<ProjectFilesWidget />`: list all files for a project (Plugin API), pick one, and optionally auto-save to your app
8
-
9
- ## Installation
10
-
11
- ```bash
12
- # Bun (recommended)
13
- bun add @thetechfossil/upfiles
14
-
15
- # npm
16
- npm install @thetechfossil/upfiles
17
-
18
- # pnpm
19
- pnpm add @thetechfossil/upfiles
20
-
21
- # yarn
22
- yarn add @thetechfossil/upfiles
23
- ```
24
-
25
- ## Server prerequisites
26
-
27
- Your Upfiles app must expose the Plugin API presign endpoint:
28
-
29
- - Method: `POST`
30
- - Path: `/api/plugin/upload/presigned-url` (default; configurable)
31
- - Body: `{ fileName, fileType, fileSize, folderPath?, projectId? }` (`fileSize` is required)
32
- - Response: `{ presignedUrl, fileKey, publicUrl, apiKeyId?, projectId? }`
33
-
34
- This repository already provides a Next.js route at `src/app/api/plugin/upload/presigned-url/route.ts`.
35
-
36
- For plugins/packages calling across origins, authenticate using an API key header as documented below. CORS must allow your plugin origin.
37
-
38
- ## Basic usage (React)
39
-
40
- ```tsx
41
- import { Uploader } from '@thetechfossil/upfiles';
42
-
43
- export default function Page() {
44
- return (
45
- <Uploader
46
- multiple
47
- accept={["image/*", "application/pdf"]}
48
- maxFileSize={100 * 1024 * 1024}
49
- onComplete={(files) => {
50
- console.log('uploaded:', files);
51
- }}
52
- onError={(err) => {
53
- console.error(err);
54
- }}
55
- dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
56
- buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
57
- // Optional: pass clientOptions for cross-origin + API key auth
58
- // clientOptions={{
59
- // baseUrl: 'https://YOUR_APP_HOST',
60
- // // or presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url',
61
- // apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
62
- // apiKeyHeader: 'authorization', // or 'x-api-key' | 'x-up-api-key'
63
- // }}
64
- />
65
- );
66
- }
67
- ```
68
-
69
- ## Basic usage (vanilla JS)
70
-
71
- ```ts
72
- import { UpfilesClient } from '@thetechfossil/upfiles';
73
-
74
- // Same-origin by default; only set these when needed:
75
- const client = new UpfilesClient({
76
- // presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url', // full override
77
- // baseUrl: 'https://YOUR_APP_HOST', // used with presignPath
78
- // presignPath: '/api/plugin/upload/presigned-url', // default
79
- // withCredentials: true, // for session routes (not typical for Plugin API)
80
- // Provide API key for Plugin API auth:
81
- // apiKey: 'upk_...'
82
- // apiKeyHeader: 'authorization' // or 'x-api-key' | 'x-up-api-key'
83
- });
84
-
85
- async function upload(file) {
86
- const { presignedUrl, publicUrl } = await client.getPresignedUrl({
87
- fileName: file.name,
88
- fileType: file.type,
89
- fileSize: file.size, // required
90
- folderPath: 'my-plugin/uploads/',
91
- });
92
- const res = await client.uploadToS3(presignedUrl, file);
93
- if (!res.ok) throw new Error('Upload failed');
94
- return publicUrl;
95
- }
96
- ```
97
-
98
- ## API
99
-
100
- - `new UpfilesClient(options)`
101
- - `presignUrl` (optional): full absolute URL to presign endpoint. Highest priority.
102
- - `baseUrl` (optional): API base origin. Used with `presignPath`.
103
- - `presignPath` (optional): relative path, default `/api/plugin/upload/presigned-url`.
104
- - `headers` (optional): extra headers to send.
105
- - `apiKey` (optional): API key value (e.g., `upk_...`).
106
- - `apiKeyHeader` (optional): one of `'authorization' | 'x-api-key' | 'x-up-api-key'` (default `'authorization'`). If `'authorization'`, `Bearer` prefix is auto-added for `upk_...`.
107
- - `withCredentials` (optional): `true` to send cookies.
108
- - Defaults: same-origin requests to `/api/plugin/upload/presigned-url`.
109
- - `client.getPresignedUrl({ fileName, fileType, fileSize, projectId?, folderPath? })`
110
- - `client.uploadToS3(presignedUrl, file)`
111
- - `client.upload(file, { projectId?, folderPath?, fetchThumbnails? })` → returns `{ publicUrl, fileKey, fileName, fileType, fileSize, projectId?, apiKeyId?, thumbnails? }`
112
- - `client.getThumbnails(fileKey)` returns `Thumbnail[]`
113
- - `client.getProjectFiles({ folderPath? })` → returns `FileListItem[]`
114
-
115
- - `<Uploader />` props
116
- - `clientOptions` (optional): same as `UpfilesClient` options (only needed for cross-origin or auth customization)
117
- - `multiple` (default true)
118
- - `accept` (array of MIME patterns)
119
- - `maxFileSize` (bytes, default 100MB)
120
- - `maxFiles` (default 10)
121
- - `fetchThumbnails` (boolean): if true, fetch thumbnails after upload via Plugin API
122
- - `onComplete(files)` and `onError(error)`
123
- - `className`, `buttonClassName`, `dropzoneClassName`
124
- - `children`: custom dropzone inner content
125
-
126
- ### Thumbnails usage
127
-
128
- Your Upfiles app exposes `GET /api/plugin/thumbnails?fileKey=...` for plugins. Enable `fetchThumbnails` to auto-fetch after upload, or call `client.getThumbnails(fileKey)` yourself.
129
-
130
- Example with `client.upload`:
131
-
132
- ```ts
133
- const result = await client.upload(file, { folderPath: 'my-plugin/', fetchThumbnails: true });
134
- console.log(result.thumbnails);
135
- ```
136
-
137
- ### List project files (Plugin API)
138
-
139
- Your Upfiles app exposes `GET /api/plugin/files?folderPath=...` for plugins (API key auth + CORS). The client can call it via `getProjectFiles`:
140
-
141
- ```ts
142
- import { UpfilesClient } from '@thetechfossil/upfiles';
143
-
144
- const client = new UpfilesClient({
145
- baseUrl: 'https://YOUR_APP_HOST',
146
- apiKey: 'upk_...',
147
- thumbnailsPath: '/api/plugin/thumbnails', // used to derive /api/plugin/files
148
- });
149
-
150
- const files = await client.getProjectFiles({ folderPath: 'optional/subfolder' });
151
- // [{ key, originalName, size, contentType, uploadedAt, url }, ...]
152
- ```
153
-
154
- ### `<ProjectFilesWidget />`
155
-
156
- Simple UI to fetch and select a file for the project tied to the API key. Emits the selection, and can optionally POST it to your backend.
157
-
158
- ```tsx
159
- import { ProjectFilesWidget } from '@thetechfossil/upfiles';
160
-
161
- export default function PickFile() {
162
- return (
163
- <ProjectFilesWidget
164
- clientOptions={{
165
- baseUrl: 'https://YOUR_APP_HOST',
166
- apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
167
- apiKeyHeader: 'authorization',
168
- thumbnailsPath: '/api/plugin/thumbnails',
169
- }}
170
- // Optional: filter within a folder
171
- // folderPath="my-plugin/"
172
-
173
- onSelect={(f) => {
174
- // { name, key, url, size, contentType }
175
- console.log('Selected:', f.name, f.key);
176
- }}
177
-
178
- // Optional: built-in save
179
- // saveUrl="/api/files/save" // Your app route to persist selection
180
- // onSave={async (f) => { await fetch('/api/files/save', { method: 'POST', body: JSON.stringify(f) }); }}
181
- // onSaved={(result) => console.log('Saved!', result)}
182
- />
183
- );
184
- }
185
- ```
186
-
187
- ## Notes
188
-
189
- - Dev with Vite: configure a proxy so `/api/*` maps to your backend (e.g., `http://localhost:4000`), then `<Uploader />` works with no config.
190
- - Ensure CORS where applicable (S3 bucket CORS; and set `PLUGIN_ALLOWED_ORIGINS` on your Upfiles app for cross-origin Plugin API calls).
191
- - For cross-origin with cookies (NextAuth), set `withCredentials: true` and configure your server to allow credentials.
192
- Plugin API typically uses API keys instead of cookies.
193
-
194
- ### Plugin endpoints provided by this repo
195
-
196
- - `POST /api/plugin/upload/presigned-url` – generate presigned S3 URL for upload
197
- - `GET /api/plugin/thumbnails?fileKey=...` list thumbnails for a file
198
- - `GET /api/plugin/files?folderPath=...` – list project files (API key scoped), optional folder prefix
199
-
200
- ## Full API Reference
201
-
202
- __UpfilesClient(options)__
203
-
204
- - __options.baseUrl?: string__
205
- Base origin, for cross-origin Plugin API calls, e.g. `https://upfiles.example.com`. If omitted, requests are same-origin.
206
- - __options.presignPath?: string__
207
- Relative path to the presign endpoint (default `/api/plugin/upload/presigned-url`). Used when `presignUrl` is not set.
208
- - __options.presignUrl?: string__
209
- Full absolute URL. If provided, it overrides `baseUrl + presignPath`.
210
- - __options.headers?: Record<string,string>__
211
- Extra headers to send with every request.
212
- - __options.withCredentials?: boolean__
213
- If true, send cookies (useful for session-based routes). Plugin API typically uses API keys instead.
214
- - __options.apiKey?: string__
215
- API key value (e.g., `upk_...`).
216
- - __options.apiKeyHeader?: 'authorization' | 'x-api-key' | 'x-up-api-key'__
217
- Header to carry the API key. Default `'authorization'`. If set to `'authorization'` and the key starts with `upk_`, `Bearer ${key}` is auto-applied.
218
- - __options.thumbnailsPath?: string__
219
- Relative path to thumbnails endpoint, default `/api/plugin/thumbnails`. Used to derive `/api/plugin/files`.
220
-
221
- Methods
222
-
223
- - __getPresignedUrl({ fileName, fileType, fileSize, projectId?, folderPath? }): Promise<PresignResponse>__
224
- Validates inputs and POSTs to the presign endpoint. Returns `{ presignedUrl, fileKey, publicUrl, apiKeyId?, projectId? }`.
225
- - __uploadToS3(presignedUrl: string, file: File): Promise<Response>__
226
- Executes the signed PUT request to S3. You are responsible for checking `res.ok`.
227
- - __upload(file: File, extras?: { projectId?, folderPath?, fetchThumbnails? }): Promise<UploadedFileResult>__
228
- Convenience method: presign + PUT + optional thumbnails fetch. Returns `{ publicUrl, fileKey, fileName, fileType, fileSize, projectId?, apiKeyId?, thumbnails? }`.
229
- - __getThumbnails(fileKey: string): Promise<Thumbnail[]>__
230
- Calls `GET /api/plugin/thumbnails?fileKey=...`.
231
- - __getProjectFiles({ folderPath? }?): Promise<FileListItem[]>__
232
- Calls `GET /api/plugin/files?folderPath=...` (derived from `thumbnailsPath`). Returns metadata for each file.
233
-
234
- Types
235
-
236
- ```ts
237
- type PresignResponse = {
238
- presignedUrl: string;
239
- fileKey: string;
240
- publicUrl: string;
241
- apiKeyId?: string;
242
- projectId?: string;
243
- };
244
-
245
- type UploadedFileResult = {
246
- publicUrl: string;
247
- fileKey: string;
248
- fileName: string;
249
- fileType: string;
250
- fileSize: number;
251
- projectId?: string;
252
- apiKeyId?: string;
253
- thumbnails?: Thumbnail[];
254
- };
255
-
256
- type Thumbnail = {
257
- id: string;
258
- key: string;
259
- url: string;
260
- size: number;
261
- sizeType?: string;
262
- createdAt?: string;
263
- };
264
-
265
- type FileListItem = {
266
- key: string;
267
- originalName: string;
268
- size: number;
269
- contentType: string;
270
- uploadedAt: string;
271
- url: string;
272
- };
273
- ```
274
-
275
- ## React Components
276
-
277
- __<Uploader />__ from `src/Uploader.tsx`
278
-
279
- - __clientOptions?: UpfilesClientOptions__
280
- Supply `baseUrl`, `apiKey`, etc., only if doing cross-origin or custom auth.
281
- - __multiple?: boolean__ (default: true)
282
- - __accept?: string[]__
283
- MIME patterns, default includes common image/video/audio/pdf/text/json.
284
- - __maxFileSize?: number__ (bytes; default: 100MB)
285
- - __maxFiles?: number__ (default: 10)
286
- - __projectId?: string__
287
- - __folderPath?: string__
288
- - __fetchThumbnails?: boolean__
289
- After a successful upload, fetch thumbnails via Plugin API.
290
- - __onComplete?: (files: UploaderFile[]) => void__
291
- Called with successful items.
292
- - __onError?: (error: Error) => void__
293
- - __className?, buttonClassName?, dropzoneClassName?__
294
- - __children?__: custom inner content for dropzone.
295
-
296
- Uploader item shape
297
-
298
- ```ts
299
- type UploaderFile = {
300
- id: string;
301
- file: File;
302
- name: string;
303
- size: number;
304
- type: string;
305
- progress: number; // 0-100
306
- status: 'pending' | 'uploading' | 'success' | 'error';
307
- error?: string;
308
- publicUrl?: string;
309
- fileKey?: string;
310
- projectId?: string;
311
- apiKeyId?: string;
312
- // thumbnails?: Thumbnail[] // added at runtime if fetchThumbnails
313
- };
314
- ```
315
-
316
- Implementation notes
317
-
318
- - Uses `XMLHttpRequest` for the S3 PUT to expose upload progress.
319
- - Progress caps at 99% until completion; then sets to 100% on success.
320
- - Sets `Content-Type` header to file.type (or `application/octet-stream`).
321
-
322
- __<ProjectFilesWidget />__ from `src/ProjectFilesWidget.tsx`
323
-
324
- - __clientOptions: UpfilesClientOptions__
325
- Provide `baseUrl` and `apiKey` (or `withCredentials`) for Plugin API.
326
- - __folderPath?: string__
327
- - __className?, listClassName?, itemClassName?__
328
- - __onSelect(file): void__
329
- Receives `{ name, key, url, size, contentType }`.
330
- - __saveUrl?: string__
331
- If set, the component will POST the selected file JSON to this URL (same-origin).
332
- - __onSave?: (file) => Promise<void> | void__
333
- - __onSaved?: (result?: any) => void__
334
-
335
- Behavior
336
-
337
- - Loads files via `client.getProjectFiles({ folderPath })` on mount and when refreshed.
338
- - Client-side search by filename.
339
- - Built-in "Use" action triggers `onSelect`, optional `onSave` and/or POST to `saveUrl`.
340
-
341
- ## Configuration and Auth
342
-
343
- - __Same-origin__ usage needs no config. Ensure your app exposes the Plugin API routes documented above.
344
- - __Cross-origin plugins__ should:
345
- - Set `baseUrl` to your Upfiles app origin.
346
- - Provide `apiKey` and set `apiKeyHeader` as needed. If using `'authorization'` and key starts with `upk_`, `Bearer` is added automatically.
347
- - Configure CORS on the Upfiles app to allow your plugin origin.
348
- - __Cookies with NextAuth or session routes__:
349
- - Set `withCredentials: true` in `UpfilesClient` and allow credentials server-side. Plugin API is usually API-key based instead.
350
-
351
- ## Error Handling
352
-
353
- - Network/API errors in `getPresignedUrl`, `getThumbnails`, `getProjectFiles` throw with a friendly message when possible.
354
- - In `<Uploader />`, per-file errors are surfaced via item `error` and `status: 'error'`, and the component continues uploading other files.
355
- - `uploadToS3` returns a Response; check `res.ok`. The convenience `upload()` will throw if the PUT fails.
356
-
357
- ## Troubleshooting
358
-
359
- - __`Failed to get upload URL`__
360
- - Check your presign route is reachable (correct `baseUrl`/`presignUrl`).
361
- - Ensure `fileSize` is provided and > 0.
362
- - Verify API key header and value; confirm CORS on the server.
363
- - __S3 PUT 403/SignatureDoesNotMatch__
364
- - Do not modify headers other than `Content-Type`.
365
- - Ensure the file name/type/size used for presign match the actual uploaded file.
366
- - __CORS errors__
367
- - Allow your plugin origin on the Upfiles app for Plugin API paths and S3 bucket CORS for PUT/GET.
368
- - __Large files stall at 99%__
369
- - Expected: progress caps at 99% until PUT resolves, then jumps to 100%.
370
- - __No files listed in ProjectFilesWidget__
371
- - Confirm the API key belongs to a project with files.
372
- - Ensure `thumbnailsPath` is correct so `/files` is derived properly.
373
-
374
- ## FAQ
375
-
376
- - __Can I use it without React?__ Yes—use `UpfilesClient` directly (see vanilla example).
377
- - __How do I restrict uploads to a folder?__ Provide `folderPath` when presigning (`<Uploader folderPath="my-plugin/" />` or `client.upload(..., { folderPath })`).
378
- - __How do I associate uploads with a project?__ Provide `projectId` when presigning or via `client.upload(..., { projectId })`.
379
- - __Can I customize headers?__ Pass `headers` in `UpfilesClientOptions`.
380
-
381
- ## Example: Minimal cross-origin plugin
382
-
383
- ```tsx
384
- import { Uploader } from '@thetechfossil/upfiles';
385
-
386
- export default function Widget() {
387
- return (
388
- <Uploader
389
- clientOptions={{
390
- baseUrl: 'https://upfiles.example.com',
391
- apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
392
- apiKeyHeader: 'authorization',
393
- }}
394
- multiple
395
- accept={['image/*']}
396
- maxFileSize={20 * 1024 * 1024}
397
- folderPath="my-plugin/"
398
- fetchThumbnails
399
- onComplete={(files) => console.log('Uploaded', files)}
400
- onError={(e) => console.error(e)}
401
- />
402
- );
403
- }
404
- ```
405
-
406
- ## Development
407
-
408
- - Build: `bun run build`
409
- - Watch: `bun run dev`
410
- - Typecheck: `bun run typecheck`
411
-
412
- ## License
413
-
414
- MIT UpFiles
1
+ # @thetechfossil/upfiles
2
+
3
+ Lightweight JavaScript client and React component to upload files via a presigned S3 flow. Similar to UploadThing-like DX.
4
+
5
+ - Client: get presigned URLs and PUT to S3
6
+ - React `<Uploader />`: drag/drop, progress, single/multiple, accepts/types, size limits
7
+ - React `<ProjectFilesWidget />`: list all files for a project (Plugin API), pick one, and optionally auto-save to your app
8
+
9
+ ## Installation
10
+
11
+ ```bash
12
+ # Bun (recommended)
13
+ bun add @thetechfossil/upfiles
14
+
15
+ # npm
16
+ npm install @thetechfossil/upfiles
17
+
18
+ # pnpm
19
+ pnpm add @thetechfossil/upfiles
20
+
21
+ # yarn
22
+ yarn add @thetechfossil/upfiles
23
+ ```
24
+
25
+ ## Quickstart
26
+
27
+ ```tsx
28
+ import { Uploader, ConnectProjectDialog } from '@thetechfossil/upfiles';
29
+ import { useState } from 'react';
30
+
31
+ export default function Page() {
32
+ const [open, setOpen] = useState(false);
33
+ const [apiKey, setApiKey] = useState<string | null>(null);
34
+
35
+ return (
36
+ <div className="space-y-6">
37
+ <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
38
+ Connect to Upfiles
39
+ </button>
40
+
41
+ <Uploader
42
+ clientOptions={{
43
+ baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
44
+ apiKey: apiKey ?? undefined, // set after connecting
45
+ apiKeyHeader: 'authorization',
46
+ }}
47
+ multiple
48
+ dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
49
+ buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
50
+ />
51
+
52
+ <ConnectProjectDialog
53
+ baseUrl={process.env.NEXT_PUBLIC_UPFILES_APP_URL!}
54
+ open={open}
55
+ onOpenChange={setOpen}
56
+ onConnected={(key) => setApiKey(key)}
57
+ />
58
+ </div>
59
+ );
60
+ }
61
+ ```
62
+
63
+ ## Server prerequisites
64
+
65
+ Your Upfiles app must expose the Plugin API presign endpoint:
66
+
67
+ - Method: `POST`
68
+ - Path: `/api/plugin/upload/presigned-url` (default; configurable)
69
+ - Body: `{ fileName, fileType, fileSize, folderPath?, projectId? }` (`fileSize` is required)
70
+ - Response: `{ presignedUrl, fileKey, publicUrl, apiKeyId?, projectId? }`
71
+
72
+ This repository already provides a Next.js route at `src/app/api/plugin/upload/presigned-url/route.ts`.
73
+
74
+ For plugins/packages calling across origins, authenticate using an API key header as documented below. CORS must allow your plugin origin.
75
+
76
+ ## Basic usage (React)
77
+
78
+ ```tsx
79
+ import { Uploader } from '@thetechfossil/upfiles';
80
+
81
+ export default function Page() {
82
+ return (
83
+ <Uploader
84
+ multiple
85
+ accept={["image/*", "application/pdf"]}
86
+ maxFileSize={100 * 1024 * 1024}
87
+ onComplete={(files) => {
88
+ console.log('uploaded:', files);
89
+ }}
90
+ onError={(err) => {
91
+ console.error(err);
92
+ }}
93
+ dropzoneClassName="border border-dashed rounded-md p-6 cursor-pointer"
94
+ buttonClassName="px-3 py-2 rounded bg-blue-600 text-white"
95
+ // Optional: pass clientOptions for cross-origin + API key auth
96
+ // clientOptions={{
97
+ // baseUrl: 'https://YOUR_APP_HOST',
98
+ // // or presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url',
99
+ // apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
100
+ // apiKeyHeader: 'authorization', // or 'x-api-key' | 'x-up-api-key'
101
+ // }}
102
+ />
103
+ );
104
+ }
105
+ ```
106
+
107
+ ## Basic usage (vanilla JS)
108
+
109
+ ```ts
110
+ import { UpfilesClient } from '@thetechfossil/upfiles';
111
+
112
+ // Same-origin by default; only set these when needed:
113
+ const client = new UpfilesClient({
114
+ // presignUrl: 'https://YOUR_APP_HOST/api/plugin/upload/presigned-url', // full override
115
+ // baseUrl: 'https://YOUR_APP_HOST', // used with presignPath
116
+ // presignPath: '/api/plugin/upload/presigned-url', // default
117
+ // withCredentials: true, // for session routes (not typical for Plugin API)
118
+ // Provide API key for Plugin API auth:
119
+ // apiKey: 'upk_...'
120
+ // apiKeyHeader: 'authorization' // or 'x-api-key' | 'x-up-api-key'
121
+ });
122
+
123
+ async function upload(file) {
124
+ const { presignedUrl, publicUrl } = await client.getPresignedUrl({
125
+ fileName: file.name,
126
+ fileType: file.type,
127
+ fileSize: file.size, // required
128
+ folderPath: 'my-plugin/uploads/',
129
+ });
130
+ const res = await client.uploadToS3(presignedUrl, file);
131
+ if (!res.ok) throw new Error('Upload failed');
132
+ return publicUrl;
133
+ }
134
+ ```
135
+
136
+ ## API
137
+
138
+ - `new UpfilesClient(options)`
139
+ - `presignUrl` (optional): full absolute URL to presign endpoint. Highest priority.
140
+ - `baseUrl` (optional): API base origin. Used with `presignPath`.
141
+ - `presignPath` (optional): relative path, default `/api/plugin/upload/presigned-url`.
142
+ - `headers` (optional): extra headers to send.
143
+ - `apiKey` (optional): API key value (e.g., `upk_...`).
144
+ - `apiKeyHeader` (optional): one of `'authorization' | 'x-api-key' | 'x-up-api-key'` (default `'authorization'`). If `'authorization'`, `Bearer` prefix is auto-added for `upk_...`.
145
+ - `withCredentials` (optional): `true` to send cookies.
146
+ - Defaults: same-origin requests to `/api/plugin/upload/presigned-url`.
147
+ - `client.getPresignedUrl({ fileName, fileType, fileSize, projectId?, folderPath? })`
148
+ - `client.uploadToS3(presignedUrl, file)`
149
+ - `client.upload(file, { projectId?, folderPath?, fetchThumbnails? })` → returns `{ publicUrl, fileKey, fileName, fileType, fileSize, projectId?, apiKeyId?, thumbnails? }`
150
+ - `client.getThumbnails(fileKey)` returns `Thumbnail[]`
151
+ - `client.getProjectFiles({ folderPath? })` returns `FileListItem[]`
152
+
153
+ - `<Uploader />` props
154
+ - `clientOptions` (optional): same as `UpfilesClient` options (only needed for cross-origin or auth customization)
155
+ - `multiple` (default true)
156
+ - `accept` (array of MIME patterns)
157
+ - `maxFileSize` (bytes, default 100MB)
158
+ - `maxFiles` (default 10)
159
+ - `fetchThumbnails` (boolean): if true, fetch thumbnails after upload via Plugin API
160
+ - `onComplete(files)` and `onError(error)`
161
+ - `className`, `buttonClassName`, `dropzoneClassName`
162
+ - `children`: custom dropzone inner content
163
+
164
+ ### Thumbnails usage
165
+
166
+ Your Upfiles app exposes `GET /api/plugin/thumbnails?fileKey=...` for plugins. Enable `fetchThumbnails` to auto-fetch after upload, or call `client.getThumbnails(fileKey)` yourself.
167
+
168
+ Example with `client.upload`:
169
+
170
+ ```ts
171
+ const result = await client.upload(file, { folderPath: 'my-plugin/', fetchThumbnails: true });
172
+ console.log(result.thumbnails);
173
+ ```
174
+
175
+ ### List project files (Plugin API)
176
+
177
+ Your Upfiles app exposes `GET /api/plugin/files?folderPath=...` for plugins (API key auth + CORS). The client can call it via `getProjectFiles`:
178
+
179
+ ```ts
180
+ import { UpfilesClient } from '@thetechfossil/upfiles';
181
+
182
+ const client = new UpfilesClient({
183
+ baseUrl: 'https://YOUR_APP_HOST',
184
+ apiKey: 'upk_...',
185
+ thumbnailsPath: '/api/plugin/thumbnails', // used to derive /api/plugin/files
186
+ });
187
+
188
+ const files = await client.getProjectFiles({ folderPath: 'optional/subfolder' });
189
+ // [{ key, originalName, size, contentType, uploadedAt, url }, ...]
190
+ ```
191
+
192
+ ### `<ProjectFilesWidget />`
193
+
194
+ Simple UI to fetch and select a file for the project tied to the API key. Emits the selection, and can optionally POST it to your backend.
195
+
196
+ ```tsx
197
+ import { ProjectFilesWidget } from '@thetechfossil/upfiles';
198
+
199
+ export default function PickFile() {
200
+ return (
201
+ <ProjectFilesWidget
202
+ clientOptions={{
203
+ baseUrl: 'https://YOUR_APP_HOST',
204
+ apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
205
+ apiKeyHeader: 'authorization',
206
+ thumbnailsPath: '/api/plugin/thumbnails',
207
+ }}
208
+ // Optional: filter within a folder
209
+ // folderPath="my-plugin/"
210
+
211
+ onSelect={(f) => {
212
+ // { name, key, url, size, contentType }
213
+ console.log('Selected:', f.name, f.key);
214
+ }}
215
+
216
+ // Optional: built-in save
217
+ // saveUrl="/api/files/save" // Your app route to persist selection
218
+ // onSave={async (f) => { await fetch('/api/files/save', { method: 'POST', body: JSON.stringify(f) }); }}
219
+ // onSaved={(result) => console.log('Saved!', result)}
220
+ />
221
+ );
222
+ }
223
+ ```
224
+
225
+ ## Notes
226
+
227
+ - Dev with Vite: configure a proxy so `/api/*` maps to your backend (e.g., `http://localhost:4000`), then `<Uploader />` works with no config.
228
+ - Ensure CORS where applicable (S3 bucket CORS; and set `PLUGIN_ALLOWED_ORIGINS` on your Upfiles app for cross-origin Plugin API calls).
229
+ - For cross-origin with cookies (NextAuth), set `withCredentials: true` and configure your server to allow credentials.
230
+ Plugin API typically uses API keys instead of cookies.
231
+
232
+ ### Plugin endpoints provided by this repo
233
+
234
+ - `POST /api/plugin/upload/presigned-url` – generate presigned S3 URL for upload
235
+ - `GET /api/plugin/thumbnails?fileKey=...` – list thumbnails for a file
236
+ - `GET /api/plugin/files?folderPath=...` – list project files (API key scoped), optional folder prefix
237
+
238
+ ## Connect a project to get an API key
239
+
240
+ This package ships a ready-made dialog built with Radix UI + Tailwind classes to help developers connect their project to your main Upfiles app and obtain an API key.
241
+
242
+ ### Install
243
+
244
+ Your app must have Tailwind configured. The dialog uses Radix primitives (shadcn/ui compatible). Ensure the peer dependency is available in the host app:
245
+
246
+ ```bash
247
+ bun add @thetechfossil/upfiles @radix-ui/react-dialog
248
+ # or
249
+ npm install @thetechfossil/upfiles @radix-ui/react-dialog
250
+ ```
251
+
252
+ ### `ConnectProjectDialog` (UI modal)
253
+
254
+ Props:
255
+
256
+ - `baseUrl`: your Upfiles app origin (e.g., `https://upfiles.example.com`).
257
+ - `open`, `onOpenChange`: control the dialog.
258
+ - `onConnected(apiKey, { projectId?, source })`: receives the plaintext API key and metadata.
259
+
260
+ It presents three options:
261
+
262
+ - Connect to existing project → lists projects via `GET /api/projects`, then creates a key via `POST /api/projects/:id/keys`.
263
+ - Add API key manually → text input to paste an existing key.
264
+ - Create new project → `POST /api/projects`, then `POST /api/projects/:id/keys`.
265
+
266
+ ```tsx
267
+ import { useState } from 'react';
268
+ import { ConnectProjectDialog } from '@thetechfossil/upfiles';
269
+
270
+ export default function ConnectKeyButton() {
271
+ const [open, setOpen] = useState(false);
272
+ const [apiKey, setApiKey] = useState<string | null>(null);
273
+
274
+ return (
275
+ <div>
276
+ <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
277
+ Connect Upfiles
278
+ </button>
279
+
280
+ <ConnectProjectDialog
281
+ baseUrl={process.env.NEXT_PUBLIC_UPFILES_APP_URL!}
282
+ open={open}
283
+ onOpenChange={setOpen}
284
+ onConnected={(key, meta) => {
285
+ setApiKey(key);
286
+ // Persist the key in your own DB as needed (see below)
287
+ console.log('Connected via', meta.source, 'projectId', meta.projectId);
288
+ }}
289
+ />
290
+ </div>
291
+ );
292
+ }
293
+ ```
294
+
295
+ ### Store the key in your own DB
296
+
297
+ You control persistence. Example Next.js route handler to store an encrypted key for the current user:
298
+
299
+ ```ts
300
+ // app/api/integrations/upfiles/key/route.ts
301
+ import { NextRequest, NextResponse } from 'next/server';
302
+ import { prisma } from '@/lib/prisma';
303
+ import { getServerSession } from 'next-auth';
304
+ import { authOptions } from '@/lib/auth';
305
+
306
+ export async function POST(req: NextRequest) {
307
+ const session = await getServerSession(authOptions as any);
308
+ const userId = (session as any)?.user?.id as string | undefined;
309
+ if (!userId) return NextResponse.json({ error: 'Unauthorized' }, { status: 401 });
310
+
311
+ const { apiKey } = await req.json();
312
+ if (!apiKey) return NextResponse.json({ error: 'apiKey required' }, { status: 400 });
313
+
314
+ // TODO: encrypt before saving
315
+ await prisma.user.update({ where: { id: userId }, data: { upfilesApiKey: apiKey } });
316
+ return NextResponse.json({ ok: true });
317
+ }
318
+ ```
319
+
320
+ From `onConnected`, call this route to persist:
321
+
322
+ ```ts
323
+ await fetch('/api/integrations/upfiles/key', {
324
+ method: 'POST',
325
+ headers: { 'content-type': 'application/json' },
326
+ body: JSON.stringify({ apiKey })
327
+ });
328
+ ```
329
+
330
+ ### Utility functions (no UI)
331
+
332
+ If you prefer to build a custom UI, use the exported functions:
333
+
334
+ ```ts
335
+ import {
336
+ listProjects,
337
+ createProject,
338
+ connectProject,
339
+ addApiKeyManually,
340
+ createClientWithKey,
341
+ } from '@thetechfossil/upfiles';
342
+
343
+ const baseUrl = 'https://upfiles.example.com';
344
+
345
+ // 1) List existing projects (requires session cookie on baseUrl)
346
+ const projects = await listProjects({ baseUrl });
347
+
348
+ // 2) Create a new project and key
349
+ const { apiKey, projectId } = await createProject({ baseUrl, name: 'My App' });
350
+
351
+ // 3) Create a key for an existing project
352
+ const key2 = await connectProject({ baseUrl, projectId: 'proj_123' });
353
+
354
+ // 4) Manual
355
+ const manual = addApiKeyManually('upk_...');
356
+
357
+ // 5) Build an upload client with the key
358
+ const client = createClientWithKey(baseUrl, apiKey);
359
+ ```
360
+
361
+ ### `<ImageManager />` props
362
+
363
+ ```ts
364
+ type ImageManagerProps = {
365
+ open: boolean;
366
+ onOpenChange: (open: boolean) => void;
367
+ clientOptions: UpfilesClientOptions;
368
+ projectId?: string;
369
+ folderPath?: string;
370
+ title?: string;
371
+ description?: string;
372
+ className?: string;
373
+ gridClassName?: string;
374
+ onSelect: (image: {
375
+ url: string;
376
+ key: string;
377
+ originalName: string;
378
+ size: number;
379
+ contentType: string;
380
+ thumbnails?: { id: string; key: string; url: string; size: number; sizeType?: string }[];
381
+ }) => void;
382
+ onDelete?: (key: string) => Promise<void>;
383
+ deleteUrl?: string;
384
+ autoRecordToDb?: boolean;
385
+ fetchThumbnails?: boolean;
386
+ maxFileSize?: number;
387
+ maxFiles?: number;
388
+ mode?: 'full' | 'browse' | 'upload'; // default: 'full'
389
+ showDelete?: boolean; // default: true
390
+ };
391
+ ```
392
+
393
+ ### Image picker modal (Browse Mode)
394
+
395
+ You can use `ImageManager` in `browse` mode to let users select existing images without the upload tab:
396
+
397
+ ```tsx
398
+ import { useState } from 'react';
399
+ import { ImageManager } from '@thetechfossil/upfiles';
400
+
401
+ export default function PickImage() {
402
+ const [open, setOpen] = useState(false);
403
+ return (
404
+ <div>
405
+ <button className="px-3 py-2 rounded bg-blue-600 text-white" onClick={() => setOpen(true)}>
406
+ Pick image
407
+ </button>
408
+ <ImageManager
409
+ open={open}
410
+ onOpenChange={setOpen}
411
+ mode="browse"
412
+ clientOptions={{
413
+ baseUrl: process.env.NEXT_PUBLIC_UPFILES_APP_URL!,
414
+ apiKey: process.env.NEXT_PUBLIC_UPFILES_API_KEY!,
415
+ apiKeyHeader: 'authorization',
416
+ }}
417
+ // folderPath="my-plugin/images/" // optional
418
+ onSelect={async (img) => {
419
+ // Persist original image URL in your DB
420
+ await fetch('/api/media/save', {
421
+ method: 'POST',
422
+ headers: { 'content-type': 'application/json' },
423
+ body: JSON.stringify({ url: img.url, key: img.key }),
424
+ });
425
+ }}
426
+ />
427
+ </div>
428
+ );
429
+ }
430
+ ```
431
+
432
+ ## Reusing across multiple Next.js projects
433
+
434
+ - Install `@thetechfossil/upfiles` and `@radix-ui/react-dialog` in each project.
435
+ - Set an environment variable like `NEXT_PUBLIC_UPFILES_APP_URL` to point all consumers to your main Upfiles app.
436
+ - Each consumer stores its own copy of the API key in its DB, obtained via `ConnectProjectDialog`.
437
+
438
+ ## Troubleshooting
439
+
440
+ - __401 Unauthorized when listing/creating projects__: The `projects` and `keys` endpoints require a signed-in session on the main app domain. Open the dialog from a page that can send cookies to `{baseUrl}` or run from the same origin; otherwise configure auth/CORS accordingly.
441
+ - __CORS errors calling main app__: Ensure the main app allows your consumer origin in CORS and, if you use cookies, sets the correct `Access-Control-Allow-Credentials` headers and SameSite attributes.
442
+ - __Uploads fail__: Verify S3 bucket CORS and that `POST /api/plugin/upload/presigned-url` is accessible from the consumer app.
443
+ - __API key header__: Default is `Authorization: Bearer upk_...`. If your server expects a different header, set `apiKeyHeader: 'x-api-key' | 'x-up-api-key'` in `UpfilesClient` options.