uploader-sdk 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 ADDED
@@ -0,0 +1,477 @@
1
+ # uploader-sdk
2
+
3
+ Browser upload SDK for React and web apps with IndexedDB staging, adapter-driven transport, parallel uploads, retries, pause/resume/cancel, and progress callbacks.
4
+
5
+ This README is written for teams adopting the SDK in production. It explains:
6
+
7
+ - What the SDK does and does not do
8
+ - What your team must manage
9
+ - Tradeoffs of the current architecture
10
+ - Operational and reliability guidance
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ npm install uploader-sdk
16
+ ```
17
+
18
+ ## TL;DR
19
+
20
+ - Use this SDK when you want upload orchestration in the browser.
21
+ - Keep auth, authorization, and storage policy in your upload service API.
22
+ - Use the built-in presigned S3 adapter or provide your own adapter.
23
+
24
+ ## Current Version Scope
25
+
26
+ - Built-in provider support in this version is Amazon S3 using presigned URL flows.
27
+ - The included adapter (`uploader-sdk/adapters/presigned-s3`) expects service endpoints that issue presigned URLs for simple and multipart uploads.
28
+ - The SDK core is provider-agnostic, but non-S3 providers require your own custom adapter implementation.
29
+ - This package does not include direct server-side upload execution; uploads are performed by the browser to storage using service-issued signed URLs.
30
+
31
+ ## Responsibility Model
32
+
33
+ ### SDK responsibilities
34
+
35
+ - Stage files in IndexedDB.
36
+ - Enforce extension allow-list rules.
37
+ - Run concurrent uploads with retry support.
38
+ - Expose progress and lifecycle callbacks.
39
+ - Support pause, resume, cancel, and selected uploads.
40
+ - Trigger adapter cleanup on cancel or final failure.
41
+
42
+ ### Upload service responsibilities
43
+
44
+ - Authenticate callers and authorize upload scope.
45
+ - Issue signed URLs or upload tokens.
46
+ - Validate file metadata and policy constraints.
47
+ - Verify object existence and integrity.
48
+ - Keep audit logs and operational metrics.
49
+ - Manage storage lifecycle (retention, deletion, compliance).
50
+
51
+ ### Integrator responsibilities
52
+
53
+ - Build UX and business flow around the SDK.
54
+ - Configure retry and concurrency for your traffic profile.
55
+ - Decide extension allow-list policy.
56
+ - Handle callback-driven UI state and user messaging.
57
+ - Protect credentials and never expose service secrets in frontend code.
58
+
59
+ ## Security Boundary
60
+
61
+ - The SDK is auth-agnostic by design.
62
+ - Authentication and authorization must be enforced by your upload service API.
63
+ - Signing credentials must stay on the server side.
64
+ - Do not commit real credentials to source control.
65
+
66
+ ## Architecture Summary
67
+
68
+ High-level flow:
69
+
70
+ 1. User selects files in browser.
71
+ 2. SDK stages files in IndexedDB and returns staged metadata.
72
+ 3. SDK starts uploads via selected adapter.
73
+ 4. Adapter communicates with your upload service API.
74
+ 5. Service API signs/transacts with storage provider.
75
+ 6. SDK reports progress/retry/error/complete events.
76
+ 7. SDK removes staged data on success or cancel.
77
+
78
+ ## Tradeoffs and Design Decisions
79
+
80
+ ### IndexedDB staging
81
+
82
+ Pros:
83
+
84
+ - Works for large files and queued uploads.
85
+ - Survives page-level state loss better than in-memory queues.
86
+
87
+ Tradeoffs:
88
+
89
+ - Browser quota limits apply.
90
+ - Data remains local until cleared.
91
+ - Some browser environments (private mode, strict policies) may reduce reliability.
92
+
93
+ ### Adapter abstraction
94
+
95
+ Pros:
96
+
97
+ - Storage-provider agnostic SDK core.
98
+ - Easier to keep provider-specific logic outside business UI code.
99
+
100
+ Tradeoffs:
101
+
102
+ - Integrators must implement and maintain adapter semantics correctly.
103
+ - Runtime behavior quality depends on adapter implementation.
104
+
105
+ ### Client retries
106
+
107
+ Pros:
108
+
109
+ - Better resilience to transient failures.
110
+
111
+ Tradeoffs:
112
+
113
+ - Can amplify backend load if too aggressive.
114
+ - Requires sane retry delay and concurrency limits.
115
+
116
+ ### Allow-list only extension policy
117
+
118
+ Pros:
119
+
120
+ - Simple mental model and safer default when configured.
121
+
122
+ Tradeoffs:
123
+
124
+ - Files without an extension are rejected when allow-list is active.
125
+ - MIME spoofing is still possible; server-side validation remains mandatory.
126
+
127
+ ## Quick Start (Core API)
128
+
129
+ ```ts
130
+ import {
131
+ createUploadClient,
132
+ registerStorageAdapter,
133
+ setFileExtensionRules,
134
+ addFiles,
135
+ uploadFiles,
136
+ } from 'uploader-sdk';
137
+
138
+ const client = createUploadClient();
139
+
140
+ registerStorageAdapter(client, 'custom', {
141
+ async uploadFile(file, options = {}) {
142
+ // Call your upload service signing + upload flow here.
143
+ // Return URL string or detailed UploadResult.
144
+ return 'https://example.com/files/' + encodeURIComponent(file.name);
145
+ },
146
+ });
147
+
148
+ setFileExtensionRules(client, {
149
+ allowExtensions: ['jpg', 'png', 'pdf'],
150
+ });
151
+
152
+ await addFiles(client, selectedFiles);
153
+
154
+ await uploadFiles(client, 'custom', {
155
+ parallel: 3,
156
+ retry: 5,
157
+ retryDelayMs: 2000,
158
+ onProgress: (progress, fileName) => {
159
+ console.log(fileName, progress);
160
+ },
161
+ onRetry: (error, fileName, meta) => {
162
+ console.log('retry', fileName, meta.attempt, meta.maxRetries, error.message);
163
+ },
164
+ onComplete: (fileName, url) => {
165
+ console.log('uploaded', fileName, url);
166
+ },
167
+ onError: (error, fileName) => {
168
+ console.error('failed', fileName, error.code, error.message);
169
+ },
170
+ });
171
+ ```
172
+
173
+ ## React Hook
174
+
175
+ Import from subpaths:
176
+
177
+ ```ts
178
+ import { useUploader } from 'uploader-sdk/react';
179
+ import { createPresignedS3Adapter } from 'uploader-sdk/adapters/presigned-s3';
180
+ ```
181
+
182
+ ```tsx
183
+ import { useUploader } from 'uploader-sdk/react';
184
+ import { createPresignedS3Adapter } from 'uploader-sdk/adapters/presigned-s3';
185
+
186
+ function UploadWidget() {
187
+ const uploader = useUploader({
188
+ adapterName: 's3',
189
+ adapter: createPresignedS3Adapter({
190
+ apiBaseUrl: 'http://localhost:8787',
191
+ multipartThresholdBytes: 20 * 1024 * 1024,
192
+ multipartPartSizeBytes: 8 * 1024 * 1024,
193
+ }),
194
+ defaultUploadOptions: {
195
+ parallel: 3,
196
+ retry: 5,
197
+ retryDelayMs: 2000,
198
+ },
199
+ rules: {
200
+ allowExtensions: ['jpg', 'png', 'pdf'],
201
+ },
202
+ });
203
+
204
+ return (
205
+ <>
206
+ <input
207
+ type="file"
208
+ multiple
209
+ onChange={async (e) => {
210
+ const selected = Array.from(e.target.files || []);
211
+ await uploader.addFiles(selected);
212
+ e.target.value = '';
213
+ }}
214
+ />
215
+ <button onClick={() => void uploader.uploadAll()} disabled={uploader.isUploading}>
216
+ Upload All
217
+ </button>
218
+ </>
219
+ );
220
+ }
221
+ ```
222
+
223
+ Hook return shape:
224
+
225
+ - client
226
+ - files
227
+ - statusById
228
+ - logs
229
+ - isUploading
230
+ - refresh()
231
+ - addFiles(files)
232
+ - clearFiles()
233
+ - uploadAll(options?)
234
+ - uploadSelected(fileIds, options?)
235
+ - pause(fileId)
236
+ - resume(fileId)
237
+ - cancel(fileId)
238
+ - setRules(rules)
239
+
240
+ ## Adapter Contract
241
+
242
+ Required adapter method:
243
+
244
+ ```ts
245
+ interface StorageAdapter {
246
+ uploadFile(file: File, options?: Record<string, unknown>): Promise<string | UploadResult>;
247
+ }
248
+ ```
249
+
250
+ Optional adapter method:
251
+
252
+ ```ts
253
+ cleanupUploadSession?(fileId: string, meta?: { reason: 'canceled' | 'final-failure' }): Promise<void>;
254
+ ```
255
+
256
+ Current packaged adapter:
257
+
258
+ - `uploader-sdk/adapters/presigned-s3` (S3 presigned URL strategy)
259
+
260
+ Common runtime options passed to adapter:
261
+
262
+ - fileId
263
+ - fileName
264
+ - fileSize
265
+ - fileType
266
+ - signal (AbortSignal)
267
+ - attempt
268
+ - onProgress(progress: number)
269
+
270
+ ## Extension Rules
271
+
272
+ - If allowExtensions is provided, only those extensions are accepted.
273
+ - If allowExtensions is omitted, all extensions are accepted.
274
+
275
+ ## Public API Surface
276
+
277
+ - createUploadClient(config?)
278
+ - registerStorageAdapter(client, name, adapter)
279
+ - setFileExtensionRules(client, rules)
280
+ - addFiles(client, files)
281
+ - listFiles(client)
282
+ - clearFiles(client)
283
+ - uploadFiles(client, adapterName, options?)
284
+ - uploadSelectedFiles(client, adapterName, fileIds, options?)
285
+ - pauseFileUpload(client, fileId)
286
+ - resumeFileUpload(client, fileId)
287
+ - cancelFileUpload(client, fileId)
288
+
289
+ ## Error Model and Retry Semantics
290
+
291
+ Error codes:
292
+
293
+ - ADAPTER_NOT_FOUND
294
+ - UPLOAD_ABORTED
295
+ - UPLOAD_CANCELED
296
+ - UPLOAD_FAILED
297
+ - INDEXEDDB_ERROR
298
+
299
+ Retry behavior:
300
+
301
+ - onRetry fires for non-final failures.
302
+ - onError and onFinalError fire only after retries are exhausted.
303
+ - Pause-triggered abort does not consume retry budget.
304
+ - Cancel-triggered abort is treated as user intent and not surfaced as failure.
305
+
306
+ ## Cancellation and Local Data Semantics
307
+
308
+ On cancel:
309
+
310
+ - Staged file is removed from IndexedDB.
311
+ - Adapter cleanup is invoked (best effort).
312
+ - For multipart uploads, local session metadata is removed when abort succeeds.
313
+ - If abort cannot be confirmed, session metadata may be retained for retryable cleanup.
314
+
315
+ ## Operational Tuning Guidance
316
+
317
+ Recommended starting points:
318
+
319
+ - parallel: 2 to 4 for browser clients.
320
+ - retry: 3 to 5 for unstable networks.
321
+ - retryDelayMs: 500 to 2000 depending on backend limits.
322
+ - multipartPartSizeBytes: 8 MB to 16 MB for large uploads.
323
+
324
+ Tune with production metrics:
325
+
326
+ - Upload success rate
327
+ - P95/P99 upload latency
328
+ - Retry distribution
329
+ - 4xx vs 5xx failure split
330
+ - Abort cleanup success rate
331
+
332
+ ## What Developers Must Manage
333
+
334
+ Frontend/app team:
335
+
336
+ - UX state, messaging, and retry affordances.
337
+ - File picker constraints and UX limits.
338
+ - Accessibility and localization of upload UI.
339
+
340
+ Service/backend team:
341
+
342
+ - AuthN/AuthZ
343
+ - Signing and storage policy
344
+ - CORS policy for upload endpoints
345
+ - Malware/content scanning if required
346
+ - Data retention and deletion lifecycle
347
+ - Compliance controls and auditability
348
+
349
+ SRE/platform team:
350
+
351
+ - Error budgets and alerting
352
+ - Capacity/rate limiting
353
+ - Incident runbooks
354
+ - Secret rotation and key hygiene
355
+
356
+ ## Backend Contract v1 (for built-in presigned S3 adapter)
357
+
358
+ The managed upload service should expose:
359
+
360
+ 1. POST /api/presign
361
+ Request:
362
+ - fileName: string
363
+ - contentType: string
364
+ - size: number
365
+ Response:
366
+ - assetId: string
367
+ - key: string
368
+ - uploadUrl: string
369
+
370
+ 2. POST /api/multipart/start
371
+ Request:
372
+ - fileName: string
373
+ - contentType: string
374
+ - size: number
375
+ - partSize: number
376
+ Response:
377
+ - assetId: string
378
+ - key: string
379
+ - uploadId: string
380
+ - partSize: number
381
+ - totalParts: number
382
+
383
+ 3. POST /api/multipart/sign-part
384
+ Request:
385
+ - key: string
386
+ - uploadId: string
387
+ - partNumber: number
388
+ Response:
389
+ - uploadUrl: string
390
+
391
+ 4. GET /api/multipart/parts
392
+ Query:
393
+ - key: string
394
+ - uploadId: string
395
+ Response:
396
+ - parts: Array<{ PartNumber: number; ETag: string; Size?: number }>
397
+
398
+ 5. POST /api/multipart/complete
399
+ Request:
400
+ - assetId: string
401
+ - key: string
402
+ - uploadId: string
403
+ - parts: Array<{ PartNumber: number; ETag: string }>
404
+ Response:
405
+ - assetId: string | null
406
+ - key: string
407
+ - etag?: string
408
+
409
+ 6. POST /api/multipart/abort
410
+ Request:
411
+ - key: string
412
+ - uploadId: string
413
+ Response:
414
+ - ok: true
415
+
416
+ 7. POST /api/verify
417
+ Request:
418
+ - assetId: string
419
+ - key: string
420
+ - size: number
421
+ - contentType: string
422
+ Response:
423
+ - verified: boolean
424
+
425
+ 8. GET /api/download-url
426
+ Query:
427
+ - key: string
428
+ - assetId: string
429
+ Response:
430
+ - url: string
431
+
432
+ ## Failure Modes You Should Expect
433
+
434
+ - User closes tab mid-upload.
435
+ - Network flaps during multipart part transfer.
436
+ - Signed URL expiry during slow uploads.
437
+ - CORS misconfiguration blocking ETag visibility.
438
+ - Service unavailable during abort cleanup.
439
+
440
+ Plan for these with retries, user messaging, and operational alerts.
441
+
442
+ ## Testing Strategy
443
+
444
+ Local CI checks:
445
+
446
+ - npm run test:ci
447
+ - npm run release:check
448
+
449
+ Optional live integration against real service and S3:
450
+
451
+ ```bash
452
+ LIVE_S3_INTEGRATION=1 LIVE_API_BASE_URL=http://localhost:8787 npm run test:live
453
+ ```
454
+
455
+ Live check validates:
456
+
457
+ - /health availability
458
+ - end-to-end simple upload contract
459
+ - multipart abort cleanup path
460
+
461
+ By default npm run test:live is a no-op unless LIVE_S3_INTEGRATION=1 is set.
462
+
463
+ ## Package Contents
464
+
465
+ Published package includes:
466
+
467
+ - dist/*
468
+ - README.md
469
+ - package.json
470
+
471
+ Source, app demo, backend example, tests, scripts, and .env are not part of published artifacts.
472
+
473
+ ## Notes
474
+
475
+ - This package is browser-oriented and uses IndexedDB.
476
+ - Signing, auth, and storage policy belong to your managed upload service.
477
+ - Keep secrets server-side and rotate credentials if they are ever exposed.
@@ -0,0 +1,121 @@
1
+ export type UploadResult = {
2
+ url: string;
3
+ assetId?: string;
4
+ key?: string;
5
+ verified?: boolean;
6
+ };
7
+ export type UploadErrorCode = 'ADAPTER_NOT_FOUND' | 'UPLOAD_ABORTED' | 'UPLOAD_CANCELED' | 'UPLOAD_FAILED' | 'INDEXEDDB_ERROR';
8
+ export type UploadError = Error & {
9
+ code: UploadErrorCode;
10
+ retriable?: boolean;
11
+ cause?: unknown;
12
+ };
13
+ export interface StorageAdapter {
14
+ uploadFile(file: File, options?: Record<string, unknown>): Promise<string | UploadResult>;
15
+ cleanupUploadSession?: (fileId: string, meta?: {
16
+ reason: 'canceled' | 'final-failure';
17
+ }) => Promise<void>;
18
+ }
19
+ export type ExtensionRules = {
20
+ allowExtensions?: string[];
21
+ };
22
+ export type UploadOptions = {
23
+ parallel?: number;
24
+ retry?: number;
25
+ retryDelayMs?: number;
26
+ onProgress?: (progress: number, fileName: string, meta?: {
27
+ fileId: string;
28
+ }) => void;
29
+ onRetry?: (error: UploadError, fileName: string, meta: {
30
+ attempt: number;
31
+ maxRetries: number;
32
+ fileId: string;
33
+ }) => void;
34
+ onError?: (error: UploadError, fileName: string, meta?: {
35
+ fileId: string;
36
+ }) => void;
37
+ onFinalError?: (error: UploadError, fileName: string, meta?: {
38
+ fileId: string;
39
+ }) => void;
40
+ onComplete?: (fileName: string, url: string, meta?: {
41
+ fileId: string;
42
+ }) => void;
43
+ onCompleteDetailed?: (fileName: string, result: UploadResult, meta?: {
44
+ fileId: string;
45
+ }) => void;
46
+ };
47
+ export declare function createUploadError(code: UploadErrorCode, message: string, options?: {
48
+ retriable?: boolean;
49
+ cause?: unknown;
50
+ }): UploadError;
51
+ export declare class UploadSDK {
52
+ private adapters;
53
+ private dbName;
54
+ private storeName;
55
+ private allowExtensions;
56
+ private pausedFileIds;
57
+ private canceledFileIds;
58
+ private activeControllers;
59
+ constructor(config?: {
60
+ dbName?: string;
61
+ storeName?: string;
62
+ });
63
+ registerAdapter(name: string, adapter: StorageAdapter): void;
64
+ setExtensionRules(rules: ExtensionRules): void;
65
+ pauseUpload(fileId: string): void;
66
+ resumeUpload(fileId: string): void;
67
+ cancelUpload(fileId: string): Promise<void>;
68
+ saveFilesToIndexedDB(files: File[]): Promise<{
69
+ saved: string[];
70
+ rejected: string[];
71
+ }>;
72
+ listStoredFiles(): Promise<Array<{
73
+ id: string;
74
+ name: string;
75
+ size: number;
76
+ type: string;
77
+ }>>;
78
+ clearStoredFiles(): Promise<void>;
79
+ uploadFiles(adapterName: string, options?: UploadOptions): Promise<void>;
80
+ uploadSelectedFiles(adapterName: string, fileIds: string[], options?: UploadOptions): Promise<void>;
81
+ private uploadWithConcurrency;
82
+ private uploadSingleWithRetry;
83
+ private waitIfPaused;
84
+ private prepareUploadItems;
85
+ private removeStoredFileById;
86
+ private isFileAllowed;
87
+ private extractExtension;
88
+ private normalizeExtension;
89
+ private buildFileId;
90
+ private openDB;
91
+ private requestToPromise;
92
+ private txComplete;
93
+ private sleep;
94
+ private isAbortError;
95
+ private normalizeUploadError;
96
+ private cleanupAdapterSession;
97
+ private cleanupAllAdapterSessions;
98
+ }
99
+ export type UploadClient = UploadSDK;
100
+ export declare function createUploadClient(config?: {
101
+ dbName?: string;
102
+ storeName?: string;
103
+ }): UploadClient;
104
+ export declare function registerStorageAdapter(client: UploadClient, name: string, adapter: StorageAdapter): void;
105
+ export declare function setFileExtensionRules(client: UploadClient, rules: ExtensionRules): void;
106
+ export declare function addFiles(client: UploadClient, files: File[]): Promise<{
107
+ saved: string[];
108
+ rejected: string[];
109
+ }>;
110
+ export declare function listFiles(client: UploadClient): Promise<Array<{
111
+ id: string;
112
+ name: string;
113
+ size: number;
114
+ type: string;
115
+ }>>;
116
+ export declare function clearFiles(client: UploadClient): Promise<void>;
117
+ export declare function uploadFiles(client: UploadClient, adapterName: string, options?: UploadOptions): Promise<void>;
118
+ export declare function uploadSelectedFiles(client: UploadClient, adapterName: string, fileIds: string[], options?: UploadOptions): Promise<void>;
119
+ export declare function pauseFileUpload(client: UploadClient, fileId: string): void;
120
+ export declare function resumeFileUpload(client: UploadClient, fileId: string): void;
121
+ export declare function cancelFileUpload(client: UploadClient, fileId: string): Promise<void>;