@useupup/core 3.3.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.
Files changed (118) hide show
  1. package/LICENSE +21 -0
  2. package/NOTICE +8 -0
  3. package/README.md +75 -0
  4. package/dist/browser-SXR426OQ.cjs +10 -0
  5. package/dist/browser-WD6T5ABJ.js +1 -0
  6. package/dist/chunk-2H5ICKFT.cjs +122 -0
  7. package/dist/chunk-6HF3I2JS.cjs +39 -0
  8. package/dist/chunk-6S5DQDQH.js +212 -0
  9. package/dist/chunk-BMQC27OE.cjs +92 -0
  10. package/dist/chunk-BSGQGINR.cjs +219 -0
  11. package/dist/chunk-CPKZ2CXD.cjs +179 -0
  12. package/dist/chunk-DTKDIMKT.cjs +573 -0
  13. package/dist/chunk-DYE5XJ6Z.cjs +2134 -0
  14. package/dist/chunk-E3ZCNOXG.cjs +74 -0
  15. package/dist/chunk-FEQY2WYY.cjs +2377 -0
  16. package/dist/chunk-FMURBCZ7.js +120 -0
  17. package/dist/chunk-FYQQRULL.js +37 -0
  18. package/dist/chunk-GE4LIVXP.js +133 -0
  19. package/dist/chunk-HFR5HZJZ.js +56 -0
  20. package/dist/chunk-I7ULPCA2.js +72 -0
  21. package/dist/chunk-MHYXDLZR.js +569 -0
  22. package/dist/chunk-N2SLHVAW.cjs +61 -0
  23. package/dist/chunk-OOPDEOTM.js +169 -0
  24. package/dist/chunk-PMBR3OUW.js +2359 -0
  25. package/dist/chunk-QK7HZIVY.cjs +144 -0
  26. package/dist/chunk-T2AQHY7V.cjs +59 -0
  27. package/dist/chunk-TRCP5FUR.js +90 -0
  28. package/dist/chunk-U7KL5M2H.cjs +374 -0
  29. package/dist/chunk-UXNPM54W.js +2108 -0
  30. package/dist/chunk-VV6QCQLL.js +365 -0
  31. package/dist/chunk-ZMS2HXLB.js +56 -0
  32. package/dist/context-shapes-123dd3b6.d.cts +207 -0
  33. package/dist/context-shapes-27b24c8f.d.ts +207 -0
  34. package/dist/contracts-pipeline-cf177270.d.cts +78 -0
  35. package/dist/contracts-pipeline-cf177270.d.ts +78 -0
  36. package/dist/contracts-strategies-378a16b7.d.cts +168 -0
  37. package/dist/contracts-strategies-378a16b7.d.ts +168 -0
  38. package/dist/contracts.cjs +181 -0
  39. package/dist/contracts.d.cts +11 -0
  40. package/dist/contracts.d.ts +11 -0
  41. package/dist/contracts.js +4 -0
  42. package/dist/core-141335ac.d.ts +651 -0
  43. package/dist/core-bea2ceca.d.cts +651 -0
  44. package/dist/create-worker-provider-AA5SRII6.js +58 -0
  45. package/dist/create-worker-provider-KJNUSSTW.cjs +60 -0
  46. package/dist/eligibility-ONLNJ6FB.cjs +10 -0
  47. package/dist/eligibility-ULFC3E4G.js +8 -0
  48. package/dist/error-code-map-9c44a425.d.ts +62 -0
  49. package/dist/error-code-map-dd5becfa.d.cts +62 -0
  50. package/dist/i18n/index.cjs +90 -0
  51. package/dist/i18n/index.d.cts +3 -0
  52. package/dist/i18n/index.d.ts +3 -0
  53. package/dist/i18n/index.js +1 -0
  54. package/dist/index.cjs +1770 -0
  55. package/dist/index.d.cts +280 -0
  56. package/dist/index.d.ts +280 -0
  57. package/dist/index.js +1589 -0
  58. package/dist/internal.cjs +2407 -0
  59. package/dist/internal.d.cts +595 -0
  60. package/dist/internal.d.ts +595 -0
  61. package/dist/internal.js +2275 -0
  62. package/dist/pipeline/index.cjs +10 -0
  63. package/dist/pipeline/index.d.cts +11 -0
  64. package/dist/pipeline/index.d.ts +11 -0
  65. package/dist/pipeline/index.js +1 -0
  66. package/dist/pipeline-worker.cjs +199 -0
  67. package/dist/pipeline-worker.d.cts +2 -0
  68. package/dist/pipeline-worker.d.ts +2 -0
  69. package/dist/pipeline-worker.js +197 -0
  70. package/dist/resolve-locale-81feef21.d.ts +20 -0
  71. package/dist/resolve-locale-9cb63600.d.cts +20 -0
  72. package/dist/resolve-theme-01758105.d.ts +53 -0
  73. package/dist/resolve-theme-66cf8d98.d.cts +53 -0
  74. package/dist/server-mode-drive-controller-b5f8ee12.d.cts +180 -0
  75. package/dist/server-mode-drive-controller-b5f8ee12.d.ts +180 -0
  76. package/dist/steps/compress.cjs +66 -0
  77. package/dist/steps/compress.d.cts +10 -0
  78. package/dist/steps/compress.d.ts +10 -0
  79. package/dist/steps/compress.js +64 -0
  80. package/dist/steps/exif.cjs +44 -0
  81. package/dist/steps/exif.d.cts +5 -0
  82. package/dist/steps/exif.d.ts +5 -0
  83. package/dist/steps/exif.js +42 -0
  84. package/dist/steps/hash.cjs +44 -0
  85. package/dist/steps/hash.d.cts +5 -0
  86. package/dist/steps/hash.d.ts +5 -0
  87. package/dist/steps/hash.js +42 -0
  88. package/dist/steps/heic.cjs +59 -0
  89. package/dist/steps/heic.d.cts +5 -0
  90. package/dist/steps/heic.d.ts +5 -0
  91. package/dist/steps/heic.js +57 -0
  92. package/dist/steps/thumbnail.cjs +54 -0
  93. package/dist/steps/thumbnail.d.cts +10 -0
  94. package/dist/steps/thumbnail.d.ts +10 -0
  95. package/dist/steps/thumbnail.js +52 -0
  96. package/dist/strategies/index.cjs +82 -0
  97. package/dist/strategies/index.d.cts +187 -0
  98. package/dist/strategies/index.d.ts +187 -0
  99. package/dist/strategies/index.js +60 -0
  100. package/dist/strategies/tus-upload.cjs +14 -0
  101. package/dist/strategies/tus-upload.d.cts +15 -0
  102. package/dist/strategies/tus-upload.d.ts +15 -0
  103. package/dist/strategies/tus-upload.js +5 -0
  104. package/dist/theme/index.cjs +38 -0
  105. package/dist/theme/index.d.cts +3 -0
  106. package/dist/theme/index.d.ts +3 -0
  107. package/dist/theme/index.js +1 -0
  108. package/dist/theme-store-2e56a674.d.cts +39 -0
  109. package/dist/theme-store-f13e0315.d.ts +39 -0
  110. package/dist/token-endpoint-5fdb4a35.d.ts +20 -0
  111. package/dist/token-endpoint-a7d15ab6.d.cts +20 -0
  112. package/dist/ui-translations-c5d00392.d.cts +455 -0
  113. package/dist/ui-translations-c5d00392.d.ts +455 -0
  114. package/dist/uploader-props-72242002.d.ts +340 -0
  115. package/dist/uploader-props-7f956e60.d.cts +340 -0
  116. package/dist/vars-d3421f6c.d.cts +240 -0
  117. package/dist/vars-d3421f6c.d.ts +240 -0
  118. package/package.json +182 -0
@@ -0,0 +1,2359 @@
1
+ import { UpupConfigError, UpupValidationError, UpupError, UpupNetworkError, UpupAuthError } from './chunk-GE4LIVXP.js';
2
+
3
+ // src/events.ts
4
+ var EventEmitter = class {
5
+ constructor() {
6
+ this.handlers = /* @__PURE__ */ new Map();
7
+ }
8
+ on(event, handler) {
9
+ let handlers = this.handlers.get(event);
10
+ if (!handlers) {
11
+ handlers = /* @__PURE__ */ new Set();
12
+ this.handlers.set(event, handlers);
13
+ }
14
+ handlers.add(handler);
15
+ return () => {
16
+ this.off(event, handler);
17
+ };
18
+ }
19
+ off(event, handler) {
20
+ this.handlers.get(event)?.delete(handler);
21
+ }
22
+ emit(event, payload) {
23
+ const handlers = this.handlers.get(event);
24
+ if (handlers) {
25
+ for (const handler of handlers) {
26
+ try {
27
+ handler(payload);
28
+ } catch (err) {
29
+ if (typeof process !== "undefined" && process.env.NODE_ENV !== "production") {
30
+ console.error(
31
+ `[upup] listener for "${event}" threw:`,
32
+ err
33
+ );
34
+ }
35
+ }
36
+ }
37
+ }
38
+ }
39
+ removeAllListeners(event) {
40
+ if (event) {
41
+ this.handlers.delete(event);
42
+ } else {
43
+ this.handlers.clear();
44
+ }
45
+ }
46
+ };
47
+
48
+ // src/plugin.ts
49
+ var PluginManager = class {
50
+ constructor() {
51
+ this.plugins = /* @__PURE__ */ new Map();
52
+ this.extensions = /* @__PURE__ */ new Map();
53
+ }
54
+ register(plugin) {
55
+ if (this.plugins.has(plugin.name)) {
56
+ throw new UpupConfigError(
57
+ `Plugin "${plugin.name}" is already registered`,
58
+ "PLUGIN_ALREADY_REGISTERED"
59
+ );
60
+ }
61
+ this.plugins.set(plugin.name, plugin);
62
+ }
63
+ registerExtension(name, methods) {
64
+ if (this.extensions.has(name)) {
65
+ throw new UpupConfigError(
66
+ `Extension "${name}" is already registered`,
67
+ "EXTENSION_ALREADY_REGISTERED"
68
+ );
69
+ }
70
+ this.extensions.set(name, methods);
71
+ }
72
+ getPlugin(name) {
73
+ return this.plugins.get(name);
74
+ }
75
+ getExtension(name) {
76
+ return this.extensions.get(name);
77
+ }
78
+ getExtensions() {
79
+ return Object.fromEntries(this.extensions);
80
+ }
81
+ destroy() {
82
+ this.plugins.clear();
83
+ this.extensions.clear();
84
+ }
85
+ };
86
+
87
+ // src/validate-file-restrictions.ts
88
+ function fileSizeInBytes(size) {
89
+ const units = {
90
+ B: 1,
91
+ KB: 1024,
92
+ MB: 1024 ** 2,
93
+ GB: 1024 ** 3
94
+ };
95
+ return size.size * (units[size.unit] ?? 1);
96
+ }
97
+ function matchesAccept(file, accept) {
98
+ const types = accept.split(",").map((t) => t.trim()).filter(Boolean);
99
+ if (types.length === 0 || types.some((type) => type === "*" || type === "*/*")) {
100
+ return true;
101
+ }
102
+ return types.some((type) => {
103
+ if (type.endsWith("/*")) {
104
+ return file.type.startsWith(type.replace("/*", "/"));
105
+ }
106
+ if (type.startsWith(".")) {
107
+ return file.name.toLowerCase().endsWith(type.toLowerCase());
108
+ }
109
+ return file.type === type;
110
+ });
111
+ }
112
+ function validateFileRestrictions(file, options) {
113
+ const errors = [];
114
+ if (options.allowedFileTypes && !matchesAccept(file, options.allowedFileTypes)) {
115
+ errors.push({
116
+ code: "TYPE_MISMATCH" /* TYPE_MISMATCH */,
117
+ message: `File type "${file.type}" is not accepted`
118
+ });
119
+ }
120
+ if (options.maxFileSize) {
121
+ const maxBytes = fileSizeInBytes(options.maxFileSize);
122
+ if (file.size > maxBytes) {
123
+ errors.push({
124
+ code: "FILE_TOO_LARGE" /* FILE_TOO_LARGE */,
125
+ message: `File "${file.name}" exceeds maximum size`
126
+ });
127
+ }
128
+ }
129
+ if (options.minFileSize) {
130
+ const minBytes = fileSizeInBytes(options.minFileSize);
131
+ if (file.size < minBytes) {
132
+ errors.push({
133
+ code: "FILE_TOO_SMALL" /* FILE_TOO_SMALL */,
134
+ message: `File "${file.name}" is below minimum size`
135
+ });
136
+ }
137
+ }
138
+ return errors;
139
+ }
140
+
141
+ // src/file-manager.ts
142
+ var fileIdCounter = 0;
143
+ function generateFileId() {
144
+ return `upup-${Date.now()}-${++fileIdCounter}`;
145
+ }
146
+ async function computeContentHash(file) {
147
+ const buffer = await file.arrayBuffer();
148
+ const bytes = new Uint8Array(buffer);
149
+ const subtleCrypto = typeof crypto !== "undefined" ? crypto.subtle : void 0;
150
+ if (subtleCrypto) {
151
+ const hashBuffer = await subtleCrypto.digest("SHA-256", buffer);
152
+ return Array.from(new Uint8Array(hashBuffer)).map((b) => b.toString(16).padStart(2, "0")).join("");
153
+ }
154
+ let hash = 2166136261;
155
+ for (const byte of bytes) {
156
+ hash ^= byte;
157
+ hash = Math.imul(hash, 16777619);
158
+ }
159
+ return `fnv1a-${(hash >>> 0).toString(16).padStart(8, "0")}`;
160
+ }
161
+ function storedContentHash(file) {
162
+ const metadata = file.metadata;
163
+ return (typeof metadata?.originalContentHash === "string" ? metadata.originalContentHash : void 0) ?? (typeof metadata?.checksum === "string" ? metadata.checksum : void 0) ?? // eslint-disable-next-line @typescript-eslint/no-deprecated -- v3 removal tracked
164
+ file.checksumSHA256 ?? // eslint-disable-next-line @typescript-eslint/no-deprecated -- v3 removal tracked
165
+ file.fileHash;
166
+ }
167
+ function applyContentHash(file, hash) {
168
+ const fileRecord = file;
169
+ fileRecord.fileHash = hash;
170
+ file.metadata = {
171
+ ...file.metadata,
172
+ originalContentHash: hash
173
+ };
174
+ return file;
175
+ }
176
+ function contentHashPatch(file, hash) {
177
+ const patch = {
178
+ fileHash: hash,
179
+ metadata: { ...file.metadata, originalContentHash: hash }
180
+ };
181
+ return patch;
182
+ }
183
+ function nativeToUploadFile(file, source = "local" /* LOCAL */) {
184
+ const url = typeof URL !== "undefined" && typeof URL.createObjectURL === "function" ? URL.createObjectURL(file) : void 0;
185
+ const extendedFile = file;
186
+ const relativePath = typeof extendedFile.relativePath === "string" && extendedFile.relativePath.trim() !== "" ? extendedFile.relativePath : typeof extendedFile.webkitRelativePath === "string" && extendedFile.webkitRelativePath.trim() !== "" ? extendedFile.webkitRelativePath : void 0;
187
+ const existingMetadata = extendedFile.metadata && typeof extendedFile.metadata === "object" && !Array.isArray(extendedFile.metadata) ? extendedFile.metadata : {};
188
+ const uploadFile = Object.assign(file, {
189
+ id: generateFileId(),
190
+ source,
191
+ status: "IDLE" /* IDLE */,
192
+ metadata: {
193
+ ...existingMetadata,
194
+ ...relativePath ? { relativePath } : {}
195
+ },
196
+ ...url !== void 0 ? { url } : {}
197
+ });
198
+ if (relativePath) {
199
+ try {
200
+ Object.defineProperty(uploadFile, "relativePath", {
201
+ value: relativePath,
202
+ configurable: true,
203
+ enumerable: false,
204
+ writable: true
205
+ });
206
+ } catch {
207
+ try {
208
+ uploadFile.relativePath = relativePath;
209
+ } catch {
210
+ }
211
+ }
212
+ }
213
+ return uploadFile;
214
+ }
215
+ function revokeObjectUrl(file) {
216
+ if (file?.url && file.url.startsWith("blob:") && typeof URL !== "undefined" && typeof URL.revokeObjectURL === "function") {
217
+ URL.revokeObjectURL(file.url);
218
+ }
219
+ }
220
+ var FileManager = class {
221
+ constructor(options) {
222
+ this.files = /* @__PURE__ */ new Map();
223
+ this.options = options;
224
+ }
225
+ updateOptions(options) {
226
+ this.options = {
227
+ ...this.options,
228
+ ...options
229
+ };
230
+ }
231
+ getFiles() {
232
+ return new Map(this.files);
233
+ }
234
+ /**
235
+ * Immutable file-status transition: produce a NEW UploadFile carrying the patch,
236
+ * store and return it. FileManager is the sole writer of a file's status/key/metadata
237
+ * (F-144), and the new reference lets the orchestrator's ref-diff projection track the
238
+ * transition (F-145).
239
+ *
240
+ * Constraint: object-spread (`{ ...prev }`) would strip File's blob data and methods —
241
+ * they live in internal slots / on the prototype, not as own-enumerable props — leaving
242
+ * a plain object that breaks `xhr.send(file)` in the direct-upload strategy. So we
243
+ * re-wrap the same backing bytes into a fresh File (a view, not a copy) and copy the
244
+ * UploadFile fields onto it — mirroring `cloneUploadFile` (steps/image-utils.ts). Do
245
+ * NOT "simplify" this back to a spread.
246
+ */
247
+ updateFile(id, patch) {
248
+ const prev = this.files.get(id);
249
+ if (!prev) return void 0;
250
+ const next = new File([prev], prev.name, {
251
+ type: prev.type,
252
+ lastModified: prev.lastModified
253
+ });
254
+ Object.assign(next, prev, { relativePath: prev.relativePath }, patch);
255
+ const updated = next;
256
+ this.files.set(id, updated);
257
+ return updated;
258
+ }
259
+ validateFile(file) {
260
+ const [violation] = validateFileRestrictions(file, this.options);
261
+ if (violation) {
262
+ throw new UpupValidationError(
263
+ violation.message,
264
+ violation.code,
265
+ file
266
+ );
267
+ }
268
+ }
269
+ async collectAcceptedFiles(nativeFiles) {
270
+ const accepted = [];
271
+ for (const nativeFile of nativeFiles) {
272
+ let candidate = nativeFile;
273
+ if (this.options.onBeforeFileAdded) {
274
+ const result = await this.options.onBeforeFileAdded(nativeFile);
275
+ if (result === false) continue;
276
+ if (result instanceof File) {
277
+ candidate = result;
278
+ }
279
+ }
280
+ this.validateFile(candidate);
281
+ accepted.push(candidate);
282
+ }
283
+ return accepted;
284
+ }
285
+ async deduplicateByContent(nativeFiles, existingFiles) {
286
+ if (!this.options.contentDeduplication) {
287
+ return nativeFiles.map((file) => ({ file }));
288
+ }
289
+ const seen = /* @__PURE__ */ new Set();
290
+ for (const existing of existingFiles) {
291
+ const nativeExisting = existing;
292
+ const hash = storedContentHash(existing) ?? (typeof nativeExisting.arrayBuffer === "function" ? await computeContentHash(nativeExisting) : void 0);
293
+ if (hash) {
294
+ if (storedContentHash(existing) !== hash) {
295
+ this.updateFile(
296
+ existing.id,
297
+ contentHashPatch(existing, hash)
298
+ );
299
+ }
300
+ seen.add(hash);
301
+ }
302
+ }
303
+ const accepted = [];
304
+ for (const file of nativeFiles) {
305
+ const hash = await computeContentHash(file);
306
+ if (seen.has(hash)) continue;
307
+ seen.add(hash);
308
+ accepted.push({ file, hash });
309
+ }
310
+ return accepted;
311
+ }
312
+ assertBatchFits(files, existingFiles, fallbackFile) {
313
+ if (files.length === 0) return;
314
+ const violatingFile = files[0] ?? fallbackFile;
315
+ if (!violatingFile) {
316
+ throw new UpupError(
317
+ "assertBatchFits: no file available to attach to the violation error",
318
+ "BAD_REQUEST" /* BAD_REQUEST */
319
+ );
320
+ }
321
+ const existing = [...existingFiles];
322
+ if (this.options.limit) {
323
+ const remainingSlots = this.options.limit - existing.length;
324
+ if (remainingSlots <= 0 || files.length > remainingSlots) {
325
+ throw new UpupValidationError(
326
+ `Adding ${files.length} files would exceed the limit of ${this.options.limit}`,
327
+ "LIMIT_EXCEEDED" /* LIMIT_EXCEEDED */,
328
+ violatingFile
329
+ );
330
+ }
331
+ }
332
+ if (this.options.maxTotalFileSize) {
333
+ const maxTotal = fileSizeInBytes(this.options.maxTotalFileSize);
334
+ const currentTotal = existing.reduce((sum, f) => sum + f.size, 0);
335
+ const newTotal = files.reduce((sum, f) => sum + f.size, 0);
336
+ if (currentTotal + newTotal > maxTotal) {
337
+ throw new UpupValidationError(
338
+ "Total file size exceeds maximum",
339
+ "TOTAL_SIZE_EXCEEDED" /* TOTAL_SIZE_EXCEEDED */,
340
+ violatingFile
341
+ );
342
+ }
343
+ }
344
+ }
345
+ async addFiles(nativeFiles) {
346
+ const acceptedNativeFiles = await this.collectAcceptedFiles(nativeFiles);
347
+ const dedupedFiles = await this.deduplicateByContent(
348
+ acceptedNativeFiles,
349
+ this.files.values()
350
+ );
351
+ this.assertBatchFits(
352
+ dedupedFiles.map((item) => item.file),
353
+ this.files.values(),
354
+ nativeFiles[0]
355
+ );
356
+ const accepted = dedupedFiles.map(({ file, hash }) => {
357
+ const uploadFile = nativeToUploadFile(file);
358
+ return hash ? applyContentHash(uploadFile, hash) : uploadFile;
359
+ });
360
+ for (const file of accepted) {
361
+ this.files.set(file.id, file);
362
+ }
363
+ return accepted;
364
+ }
365
+ removeFile(id) {
366
+ const file = this.files.get(id);
367
+ if (file) {
368
+ revokeObjectUrl(file);
369
+ this.files.delete(id);
370
+ }
371
+ return file;
372
+ }
373
+ replaceFile(id, file) {
374
+ const current = this.files.get(id);
375
+ if (!current) {
376
+ throw new UpupError(
377
+ `replaceFile: unknown file ID "${id}"`,
378
+ "BAD_REQUEST" /* BAD_REQUEST */
379
+ );
380
+ }
381
+ const next = "metadata" in file && "source" in file && "status" in file ? Object.assign(file, { id }) : Object.assign(nativeToUploadFile(file, current.source), {
382
+ id
383
+ });
384
+ if (current.url !== next.url) {
385
+ revokeObjectUrl(current);
386
+ }
387
+ this.files.set(id, next);
388
+ return next;
389
+ }
390
+ removeAll() {
391
+ for (const file of this.files.values()) {
392
+ revokeObjectUrl(file);
393
+ }
394
+ this.files.clear();
395
+ }
396
+ /** Bulk-apply pipeline-produced files (already UploadFile with status/key set). */
397
+ applyProcessed(files) {
398
+ for (const file of files) {
399
+ this.files.set(file.id, file);
400
+ }
401
+ }
402
+ /** Re-instate a prior snapshot: clear and repopulate from entries. */
403
+ restore(entries) {
404
+ this.files.clear();
405
+ for (const [id, file] of entries) {
406
+ this.files.set(id, file);
407
+ }
408
+ }
409
+ async setFiles(nativeFiles) {
410
+ const acceptedNativeFiles = await this.collectAcceptedFiles(nativeFiles);
411
+ const dedupedFiles = await this.deduplicateByContent(
412
+ acceptedNativeFiles,
413
+ []
414
+ );
415
+ this.assertBatchFits(
416
+ dedupedFiles.map((item) => item.file),
417
+ [],
418
+ nativeFiles[0]
419
+ );
420
+ const accepted = dedupedFiles.map(({ file, hash }) => {
421
+ const uploadFile = nativeToUploadFile(file);
422
+ return hash ? applyContentHash(uploadFile, hash) : uploadFile;
423
+ });
424
+ for (const file of this.files.values()) {
425
+ revokeObjectUrl(file);
426
+ }
427
+ this.files.clear();
428
+ for (const file of accepted) {
429
+ this.files.set(file.id, file);
430
+ }
431
+ return accepted;
432
+ }
433
+ reorderFiles(fileIds) {
434
+ if (fileIds.length !== this.files.size) {
435
+ throw new UpupError(
436
+ `reorderFiles: expected ${this.files.size} IDs but received ${fileIds.length}`,
437
+ "BAD_REQUEST" /* BAD_REQUEST */
438
+ );
439
+ }
440
+ const newMap = /* @__PURE__ */ new Map();
441
+ for (const id of fileIds) {
442
+ const file = this.files.get(id);
443
+ if (!file) {
444
+ throw new UpupError(
445
+ `reorderFiles: unknown file ID "${id}"`,
446
+ "BAD_REQUEST" /* BAD_REQUEST */
447
+ );
448
+ }
449
+ newMap.set(id, file);
450
+ }
451
+ this.files = newMap;
452
+ }
453
+ };
454
+
455
+ // src/upload-manager.ts
456
+ var UpupUploadBatchError = class extends Error {
457
+ constructor(message, errors) {
458
+ super(message);
459
+ this.name = "UpupUploadBatchError";
460
+ this.errors = errors;
461
+ }
462
+ };
463
+ var UploadManager = class {
464
+ constructor(options) {
465
+ this.abortController = new AbortController();
466
+ this.options = options;
467
+ }
468
+ async uploadAll(files) {
469
+ const { maxConcurrentUploads } = this.options;
470
+ const results = [];
471
+ const errors = [];
472
+ const queue = [...files];
473
+ let active = 0;
474
+ await new Promise((resolve) => {
475
+ const tryNext = () => {
476
+ if (queue.length === 0 && active === 0) {
477
+ resolve();
478
+ return;
479
+ }
480
+ while (active < maxConcurrentUploads && queue.length > 0) {
481
+ const file = queue.shift();
482
+ if (!file) break;
483
+ active++;
484
+ this.uploadFile(file).then((result) => {
485
+ results.push(result);
486
+ this.options.onFileComplete(file, result);
487
+ }).catch((err) => {
488
+ const error = err instanceof Error ? err : new Error(String(err));
489
+ if (this.options.onFileError) {
490
+ this.options.onFileError(file, error);
491
+ }
492
+ errors.push({ file, error });
493
+ }).finally(() => {
494
+ active--;
495
+ tryNext();
496
+ });
497
+ }
498
+ };
499
+ tryNext();
500
+ if (files.length === 0) {
501
+ resolve();
502
+ }
503
+ });
504
+ if (errors.length > 0) {
505
+ throw new UpupUploadBatchError(
506
+ `${errors.length} file upload(s) failed`,
507
+ errors
508
+ );
509
+ }
510
+ return results;
511
+ }
512
+ // A method (rather than inlining `this.abortController.signal.aborted`,
513
+ // or a getter — TS's control-flow narrowing treats a no-setter accessor
514
+ // like a readonly field) so each call is re-evaluated fresh:
515
+ // `abortController` can be replaced concurrently by `pause()`/`abort()`
516
+ // while an attempt is in flight, across the `await`s below.
517
+ checkAborted() {
518
+ return this.abortController.signal.aborted;
519
+ }
520
+ async uploadFile(file) {
521
+ const {
522
+ credentials,
523
+ uploadStrategy,
524
+ maxRetries = 3,
525
+ isSuccessfulCall
526
+ } = this.options;
527
+ const maxAttempts = maxRetries + 1;
528
+ let lastError = null;
529
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
530
+ if (this.checkAborted()) {
531
+ throw new UpupNetworkError("Upload aborted");
532
+ }
533
+ try {
534
+ this.options.onFileStart?.(file);
535
+ const plan = this.options.resolveUploadStrategy?.(file) ?? {
536
+ uploadStrategy,
537
+ presign: true
538
+ };
539
+ const currentMetadata = file.metadata;
540
+ const metadata = {
541
+ ...currentMetadata,
542
+ ...file.relativePath && currentMetadata.relativePath == null ? { relativePath: file.relativePath } : {}
543
+ };
544
+ const fileMetadata = {
545
+ name: file.name,
546
+ size: file.size,
547
+ type: file.type,
548
+ metadata
549
+ };
550
+ const presigned = plan.presign ? await credentials.getPresignedUrl(fileMetadata) : {
551
+ key: file.id,
552
+ uploadUrl: "",
553
+ expiresIn: 0
554
+ };
555
+ const result = await plan.uploadStrategy.upload(
556
+ file,
557
+ presigned,
558
+ {
559
+ onProgress: (loaded, total) => {
560
+ this.options.onProgress(file.id, loaded, total);
561
+ },
562
+ signal: this.abortController.signal
563
+ }
564
+ );
565
+ if (isSuccessfulCall) {
566
+ const syntheticResponse = {
567
+ status: 200,
568
+ headers: {},
569
+ body: result
570
+ };
571
+ const ok = await isSuccessfulCall(syntheticResponse);
572
+ if (!ok) {
573
+ throw new UpupNetworkError(
574
+ "isSuccessfulCall returned false",
575
+ 0
576
+ );
577
+ }
578
+ }
579
+ return result;
580
+ } catch (err) {
581
+ if (this.checkAborted()) {
582
+ throw err;
583
+ }
584
+ const isNetwork = err instanceof UpupNetworkError;
585
+ if (isNetwork && err.status !== void 0 && err.status >= 400 && err.status < 500) {
586
+ throw err;
587
+ }
588
+ if (isNetwork && err.status === 0) {
589
+ throw err;
590
+ }
591
+ lastError = err instanceof Error ? err : new Error(String(err));
592
+ if (attempt < maxAttempts - 1) {
593
+ await new Promise(
594
+ (r) => setTimeout(r, 100 * Math.pow(2, attempt))
595
+ );
596
+ }
597
+ }
598
+ }
599
+ const finalError = lastError ?? new UpupNetworkError("Upload failed after retries");
600
+ throw finalError;
601
+ }
602
+ abort() {
603
+ this.abortController.abort();
604
+ }
605
+ pause() {
606
+ this.abortController.abort();
607
+ this.abortController = new AbortController();
608
+ }
609
+ };
610
+
611
+ // src/utils/status-helpers.ts
612
+ function isUploadActive(status) {
613
+ return status === "UPLOADING" /* UPLOADING */ || status === "PROCESSING" /* PROCESSING */;
614
+ }
615
+ function isUploadIdle(status) {
616
+ return status === "IDLE" /* IDLE */ || status === "READY" /* READY */;
617
+ }
618
+ function isFileRemovalLocked(progress, fileStatus) {
619
+ return !!progress && fileStatus !== "PAUSED" /* PAUSED */;
620
+ }
621
+
622
+ // src/crash-recovery.ts
623
+ var STORAGE_KEY = "upup-crash-recovery";
624
+ var CrashRecoveryManager = class {
625
+ constructor(storage) {
626
+ this.storage = storage;
627
+ }
628
+ async save(snapshot) {
629
+ await this.storage.set(STORAGE_KEY, snapshot);
630
+ }
631
+ async restore() {
632
+ const data = await this.storage.get(STORAGE_KEY);
633
+ return data ?? null;
634
+ }
635
+ async clear() {
636
+ await this.storage.delete(STORAGE_KEY);
637
+ }
638
+ };
639
+ var IndexedDBStorage = class {
640
+ constructor(dbName = "upup-crash-recovery") {
641
+ this.storeName = "upup-store";
642
+ /** ONE cached connection for the storage's lifetime — never closed after
643
+ * an operation. Firefox ties Blob/File handles read from IndexedDB to
644
+ * the connection they were read over: with the old open-per-operation /
645
+ * close-on-complete lifecycle, the File revived by the crash-recovery
646
+ * restore lost its backing store shortly after the restore's transaction
647
+ * closed, and mid-resume slice reads rejected with AbortError ("The
648
+ * operation was aborted") — a Firefox-only reload-resume stall reproduced
649
+ * against both MinIO and Backblaze B2 (Chromium materializes IndexedDB
650
+ * blobs independently of the connection and never failed). The cache
651
+ * still yields: `versionchange` (a deleteDatabase/upgrade elsewhere)
652
+ * closes and drops it so the other context is never blocked, and a
653
+ * browser-initiated `close` drops it so the next operation reopens. */
654
+ this.dbPromise = null;
655
+ this.dbName = dbName;
656
+ }
657
+ getDB() {
658
+ if (this.dbPromise) return this.dbPromise;
659
+ const promise = new Promise((resolve, reject) => {
660
+ const request = indexedDB.open(this.dbName, 1);
661
+ request.onupgradeneeded = () => {
662
+ request.result.createObjectStore(this.storeName);
663
+ };
664
+ request.onsuccess = () => {
665
+ const db = request.result;
666
+ db.onversionchange = () => {
667
+ db.close();
668
+ if (this.dbPromise === promise) {
669
+ this.dbPromise = null;
670
+ }
671
+ };
672
+ db.onclose = () => {
673
+ if (this.dbPromise === promise) {
674
+ this.dbPromise = null;
675
+ }
676
+ };
677
+ resolve(db);
678
+ };
679
+ request.onerror = () => {
680
+ if (this.dbPromise === promise) {
681
+ this.dbPromise = null;
682
+ }
683
+ reject(request.error ?? new Error("IndexedDB open failed"));
684
+ };
685
+ });
686
+ this.dbPromise = promise;
687
+ return promise;
688
+ }
689
+ /** Close and release the cached connection. Safe at any time — the next
690
+ * operation reopens. `UpupCore.destroy()` calls this so an unmounted
691
+ * uploader doesn't keep the database held open (pending transactions
692
+ * still drain first per the IndexedDB spec, so a queued snapshot write
693
+ * is never cut off mid-transaction). */
694
+ close() {
695
+ const promise = this.dbPromise;
696
+ if (!promise) return;
697
+ this.dbPromise = null;
698
+ void promise.then(
699
+ (db) => {
700
+ db.close();
701
+ },
702
+ () => {
703
+ }
704
+ );
705
+ }
706
+ async get(key) {
707
+ try {
708
+ const db = await this.getDB();
709
+ return await new Promise((resolve, reject) => {
710
+ const tx = db.transaction(this.storeName, "readonly");
711
+ const store = tx.objectStore(this.storeName);
712
+ const request = store.get(key);
713
+ request.onsuccess = () => {
714
+ resolve(request.result);
715
+ };
716
+ request.onerror = () => {
717
+ reject(request.error ?? new Error("IndexedDB get failed"));
718
+ };
719
+ });
720
+ } catch {
721
+ return void 0;
722
+ }
723
+ }
724
+ async set(key, value) {
725
+ try {
726
+ const db = await this.getDB();
727
+ await new Promise((resolve, reject) => {
728
+ const tx = db.transaction(this.storeName, "readwrite");
729
+ const store = tx.objectStore(this.storeName);
730
+ const request = store.put(value, key);
731
+ request.onsuccess = () => {
732
+ resolve();
733
+ };
734
+ request.onerror = () => {
735
+ reject(request.error ?? new Error("IndexedDB put failed"));
736
+ };
737
+ });
738
+ } catch {
739
+ }
740
+ }
741
+ async delete(key) {
742
+ try {
743
+ const db = await this.getDB();
744
+ await new Promise((resolve, reject) => {
745
+ const tx = db.transaction(this.storeName, "readwrite");
746
+ const store = tx.objectStore(this.storeName);
747
+ const request = store.delete(key);
748
+ request.onsuccess = () => {
749
+ resolve();
750
+ };
751
+ request.onerror = () => {
752
+ reject(
753
+ request.error ?? new Error("IndexedDB delete failed")
754
+ );
755
+ };
756
+ });
757
+ } catch {
758
+ }
759
+ }
760
+ };
761
+
762
+ // src/drives/pkce.ts
763
+ function generateCodeVerifier(length = 128) {
764
+ const charset = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~";
765
+ const values = crypto.getRandomValues(new Uint8Array(length));
766
+ return Array.from(values, (b) => charset[b % charset.length]).join("");
767
+ }
768
+ async function generateCodeChallenge(verifier) {
769
+ const data = new TextEncoder().encode(verifier);
770
+ const digest = await crypto.subtle.digest("SHA-256", data);
771
+ return btoa(String.fromCharCode(...new Uint8Array(digest))).replace(/\+/g, "-").replace(/\//g, "_").replace(/=+$/, "");
772
+ }
773
+
774
+ // src/drives/session-storage.ts
775
+ function storageGet(key) {
776
+ if (typeof window === "undefined") return null;
777
+ try {
778
+ return sessionStorage.getItem(key);
779
+ } catch {
780
+ return null;
781
+ }
782
+ }
783
+ function storageSet(key, value) {
784
+ if (typeof window === "undefined") return;
785
+ try {
786
+ sessionStorage.setItem(key, value);
787
+ } catch {
788
+ }
789
+ }
790
+ function storageDel(key) {
791
+ if (typeof window === "undefined") return;
792
+ try {
793
+ sessionStorage.removeItem(key);
794
+ } catch {
795
+ }
796
+ }
797
+
798
+ // src/drives/popup-oauth-plugin.ts
799
+ var PopupOAuthPlugin = class {
800
+ constructor() {
801
+ this.emitter = null;
802
+ this.config = { clientId: "" };
803
+ this.accessToken = null;
804
+ this.refreshTokenValue = null;
805
+ this.tokenExpiry = 0;
806
+ this.state = "idle";
807
+ this.codeVerifier = null;
808
+ // Popup polling references
809
+ this.popupWindow = null;
810
+ this.pollTimer = null;
811
+ }
812
+ get id() {
813
+ return this.spec.id;
814
+ }
815
+ get name() {
816
+ return this.spec.id;
817
+ }
818
+ // ── Plugin lifecycle ──
819
+ configure(config) {
820
+ this.config = config;
821
+ return this;
822
+ }
823
+ getConfig() {
824
+ return this.config;
825
+ }
826
+ init(emitter) {
827
+ this.emitter = emitter;
828
+ }
829
+ destroy() {
830
+ this.cleanupPopup();
831
+ this.emitter = null;
832
+ }
833
+ // ── State management ──
834
+ getState() {
835
+ return this.state;
836
+ }
837
+ setState(newState) {
838
+ this.state = newState;
839
+ this.emitter?.emit(`${this.spec.eventPrefix}:state-change`, {
840
+ state: newState
841
+ });
842
+ }
843
+ // ── Auth: build the OAuth URL with PKCE ──
844
+ async getAuthUrl() {
845
+ const clientId = this.config.clientId;
846
+ if (!clientId) {
847
+ throw new UpupAuthError(
848
+ `${this.spec.displayName} client_id is not configured`,
849
+ this.spec.displayName
850
+ );
851
+ }
852
+ const redirectUri = this.getRedirectUri();
853
+ this.codeVerifier = generateCodeVerifier();
854
+ const challenge = await generateCodeChallenge(this.codeVerifier);
855
+ const params = new URLSearchParams({
856
+ client_id: clientId,
857
+ response_type: "code",
858
+ redirect_uri: redirectUri,
859
+ code_challenge: challenge,
860
+ code_challenge_method: "S256",
861
+ ...this.spec.authParams ?? {}
862
+ });
863
+ if (this.spec.scopes) {
864
+ params.set("scope", this.spec.scopes);
865
+ }
866
+ return `${this.spec.authUrl}?${params.toString()}`;
867
+ }
868
+ // ── Auth: exchange authorization code for tokens ──
869
+ async authenticate(code) {
870
+ const clientId = this.config.clientId;
871
+ if (!clientId) {
872
+ throw new UpupAuthError(
873
+ `${this.spec.displayName} client_id is not configured`,
874
+ this.spec.displayName
875
+ );
876
+ }
877
+ if (!this.codeVerifier) {
878
+ throw new UpupAuthError(
879
+ "No PKCE code verifier \u2014 call getAuthUrl() first",
880
+ this.spec.displayName
881
+ );
882
+ }
883
+ this.setState("authenticating");
884
+ try {
885
+ const body = new URLSearchParams({
886
+ code,
887
+ client_id: clientId,
888
+ code_verifier: this.codeVerifier,
889
+ grant_type: "authorization_code",
890
+ redirect_uri: this.getRedirectUri()
891
+ });
892
+ if (this.spec.scopes) {
893
+ body.set("scope", this.spec.scopes);
894
+ }
895
+ const res = await fetch(this.spec.tokenUrl, {
896
+ method: "POST",
897
+ headers: {
898
+ "Content-Type": "application/x-www-form-urlencoded"
899
+ },
900
+ body
901
+ });
902
+ if (!res.ok) {
903
+ const text = await res.text();
904
+ throw new UpupAuthError(
905
+ `Token exchange failed: ${text}`,
906
+ this.spec.displayName
907
+ );
908
+ }
909
+ const data = await res.json();
910
+ this.setTokens(
911
+ data.access_token,
912
+ data.refresh_token ?? null,
913
+ data.expires_in
914
+ );
915
+ let user;
916
+ try {
917
+ user = await this.fetchUserProfile();
918
+ } catch {
919
+ }
920
+ this.setState("authenticated");
921
+ this.emitter?.emit(`${this.spec.eventPrefix}:authenticated`, {
922
+ user
923
+ });
924
+ } catch (err) {
925
+ this.setState("idle");
926
+ this.emitter?.emit(`${this.spec.eventPrefix}:error`, {
927
+ error: err instanceof Error ? err : new Error(String(err)),
928
+ action: "authenticate"
929
+ });
930
+ throw err;
931
+ }
932
+ }
933
+ // ── Auth: popup-based flow (open popup, poll for redirect code) ──
934
+ async authenticateViaPopup() {
935
+ if (typeof window === "undefined") {
936
+ throw new UpupAuthError(
937
+ "authenticateViaPopup requires a browser environment",
938
+ this.spec.displayName
939
+ );
940
+ }
941
+ const url = await this.getAuthUrl();
942
+ this.cleanupPopup();
943
+ const left = (typeof window !== "undefined" ? window.screenX : 0) + 60;
944
+ const top = (typeof window !== "undefined" ? window.screenY : 0) + 60;
945
+ this.popupWindow = window.open(
946
+ url,
947
+ this.spec.popupName,
948
+ `width=800,height=600,left=${left},top=${top},toolbar=0,scrollbars=1,status=1,resizable=1`
949
+ );
950
+ if (!this.popupWindow) {
951
+ this.emitter?.emit(`${this.spec.eventPrefix}:error`, {
952
+ error: new Error("Popup was blocked by the browser"),
953
+ action: "authenticateViaPopup"
954
+ });
955
+ throw new UpupAuthError(
956
+ "Popup was blocked by the browser",
957
+ this.spec.displayName
958
+ );
959
+ }
960
+ this.setState("authenticating");
961
+ return new Promise((resolve, reject) => {
962
+ const redirectUri = this.getRedirectUri();
963
+ this.pollTimer = setInterval(() => {
964
+ void (async () => {
965
+ try {
966
+ if (!this.popupWindow || this.popupWindow.closed) {
967
+ this.cleanupPopup();
968
+ this.setState("idle");
969
+ resolve();
970
+ return;
971
+ }
972
+ let href;
973
+ try {
974
+ href = this.popupWindow.location.href;
975
+ } catch {
976
+ return;
977
+ }
978
+ if (!href.startsWith(redirectUri) || !href.includes("code=")) {
979
+ return;
980
+ }
981
+ this.cleanupPollTimer();
982
+ const code = new URL(href).searchParams.get("code");
983
+ this.popupWindow.close();
984
+ this.popupWindow = null;
985
+ if (!code) {
986
+ const err = new Error(
987
+ "No authorization code found in redirect URL"
988
+ );
989
+ this.setState("idle");
990
+ this.emitter?.emit(
991
+ `${this.spec.eventPrefix}:error`,
992
+ {
993
+ error: err,
994
+ action: "authenticateViaPopup"
995
+ }
996
+ );
997
+ reject(err);
998
+ return;
999
+ }
1000
+ await this.authenticate(code);
1001
+ resolve();
1002
+ } catch (err) {
1003
+ const message = err instanceof Error ? err.message : String(err);
1004
+ if (message.includes("cross-origin") || message.includes("Cross-Origin") || message.includes("Permission denied") || message.includes("Failed to read")) {
1005
+ return;
1006
+ }
1007
+ this.cleanupPopup();
1008
+ this.setState("idle");
1009
+ this.emitter?.emit(`${this.spec.eventPrefix}:error`, {
1010
+ error: err instanceof Error ? err : new Error(String(err)),
1011
+ action: "authenticateViaPopup"
1012
+ });
1013
+ reject(err instanceof Error ? err : new Error(message));
1014
+ }
1015
+ })();
1016
+ }, 500);
1017
+ });
1018
+ }
1019
+ // ── Auth: refresh access token ──
1020
+ async refreshAccessToken() {
1021
+ const clientId = this.config.clientId;
1022
+ if (!clientId || !this.refreshTokenValue) return null;
1023
+ try {
1024
+ const body = new URLSearchParams({
1025
+ grant_type: "refresh_token",
1026
+ refresh_token: this.refreshTokenValue,
1027
+ client_id: clientId
1028
+ });
1029
+ if (this.spec.scopes) {
1030
+ body.set("scope", this.spec.scopes);
1031
+ }
1032
+ const res = await fetch(this.spec.tokenUrl, {
1033
+ method: "POST",
1034
+ headers: {
1035
+ "Content-Type": "application/x-www-form-urlencoded"
1036
+ },
1037
+ body
1038
+ });
1039
+ if (!res.ok) {
1040
+ const text = await res.text();
1041
+ throw new UpupAuthError(
1042
+ `Token refresh failed: ${text}`,
1043
+ this.spec.displayName
1044
+ );
1045
+ }
1046
+ const data = await res.json();
1047
+ this.setTokens(
1048
+ data.access_token,
1049
+ data.refresh_token ?? this.refreshTokenValue,
1050
+ data.expires_in
1051
+ );
1052
+ return data.access_token;
1053
+ } catch (err) {
1054
+ this.emitter?.emit(`${this.spec.eventPrefix}:session-expired`, {});
1055
+ this.emitter?.emit(`${this.spec.eventPrefix}:error`, {
1056
+ error: err instanceof Error ? err : new Error(String(err)),
1057
+ action: "refreshAccessToken"
1058
+ });
1059
+ this.clearTokens();
1060
+ this.setState("session-expired");
1061
+ return null;
1062
+ }
1063
+ }
1064
+ // ── Auth: sign out ──
1065
+ signOut() {
1066
+ this.clearTokens();
1067
+ this.cleanupPopup();
1068
+ this.setState("idle");
1069
+ this.emitter?.emit(`${this.spec.eventPrefix}:signed-out`, {});
1070
+ }
1071
+ // ── Auth: restore session from sessionStorage ──
1072
+ restoreSession() {
1073
+ const token = storageGet(this.spec.storageKeys.access);
1074
+ const refresh = storageGet(this.spec.storageKeys.refresh);
1075
+ const expiry = storageGet(this.spec.storageKeys.expiry);
1076
+ if (!token) return false;
1077
+ this.accessToken = token;
1078
+ this.refreshTokenValue = refresh;
1079
+ this.tokenExpiry = expiry ? parseInt(expiry, 10) : 0;
1080
+ this.setState("authenticated");
1081
+ return true;
1082
+ }
1083
+ // ── Auth helpers ──
1084
+ isAuthenticated() {
1085
+ return this.state === "authenticated" && this.accessToken !== null;
1086
+ }
1087
+ getAccessToken() {
1088
+ return this.accessToken;
1089
+ }
1090
+ // Guarded public entry point the controller drives (F-123): swallow-to-null
1091
+ // instead of throwing, so a failed profile fetch never strands the restore
1092
+ // flow in an unhandled rejection. Requires isAuthenticated() — unlike
1093
+ // authenticate()'s internal call, which fetches before the flag flips.
1094
+ async getUserInfo() {
1095
+ if (!this.isAuthenticated()) return null;
1096
+ try {
1097
+ return await this.fetchUserProfile();
1098
+ } catch {
1099
+ return null;
1100
+ }
1101
+ }
1102
+ // ── Private: authenticated API request with proactive + reactive refresh ──
1103
+ async apiRequest(url, options, isRetry = false) {
1104
+ await this.ensureValidToken();
1105
+ const headers = new Headers(options.headers ?? {});
1106
+ headers.set("Authorization", `Bearer ${this.accessToken}`);
1107
+ const res = await fetch(url, { ...options, headers });
1108
+ if (res.ok) return res;
1109
+ const errorText = await res.text();
1110
+ if (res.status === 401 && !isRetry) {
1111
+ if (this.refreshTokenValue) {
1112
+ const newToken = await this.refreshAccessToken();
1113
+ if (newToken) {
1114
+ return this.apiRequest(url, options, true);
1115
+ }
1116
+ }
1117
+ this.emitter?.emit(`${this.spec.eventPrefix}:session-expired`, {});
1118
+ this.clearTokens();
1119
+ this.setState("session-expired");
1120
+ }
1121
+ throw new UpupNetworkError(
1122
+ `${this.spec.displayName} API error (${res.status}): ${errorText}`,
1123
+ res.status
1124
+ );
1125
+ }
1126
+ // ── Private: token management ──
1127
+ setTokens(access, refresh, expiresIn) {
1128
+ this.accessToken = access;
1129
+ this.refreshTokenValue = refresh;
1130
+ this.tokenExpiry = expiresIn ? Date.now() + expiresIn * 1e3 : 0;
1131
+ storageSet(this.spec.storageKeys.access, access);
1132
+ if (refresh) storageSet(this.spec.storageKeys.refresh, refresh);
1133
+ if (this.tokenExpiry) {
1134
+ storageSet(this.spec.storageKeys.expiry, String(this.tokenExpiry));
1135
+ }
1136
+ }
1137
+ clearTokens() {
1138
+ this.accessToken = null;
1139
+ this.refreshTokenValue = null;
1140
+ this.tokenExpiry = 0;
1141
+ storageDel(this.spec.storageKeys.access);
1142
+ storageDel(this.spec.storageKeys.refresh);
1143
+ storageDel(this.spec.storageKeys.expiry);
1144
+ }
1145
+ async ensureValidToken() {
1146
+ if (!this.accessToken) {
1147
+ throw new UpupAuthError(
1148
+ "Not authenticated \u2014 no access token",
1149
+ this.spec.displayName
1150
+ );
1151
+ }
1152
+ if (this.tokenExpiry > 0 && Date.now() > this.tokenExpiry - 6e4 && this.refreshTokenValue) {
1153
+ await this.refreshAccessToken();
1154
+ }
1155
+ }
1156
+ // ── Private: redirect URI ──
1157
+ getRedirectUri() {
1158
+ if (this.config.redirectUri) {
1159
+ return this.config.redirectUri;
1160
+ }
1161
+ if (typeof window !== "undefined") {
1162
+ return `${window.location.origin}${this.spec.redirectPath}`;
1163
+ }
1164
+ return "";
1165
+ }
1166
+ // ── Private: popup cleanup ──
1167
+ cleanupPopup() {
1168
+ this.cleanupPollTimer();
1169
+ if (this.popupWindow && !this.popupWindow.closed) {
1170
+ this.popupWindow.close();
1171
+ }
1172
+ this.popupWindow = null;
1173
+ }
1174
+ cleanupPollTimer() {
1175
+ if (this.pollTimer) {
1176
+ clearInterval(this.pollTimer);
1177
+ this.pollTimer = null;
1178
+ }
1179
+ }
1180
+ };
1181
+
1182
+ // src/drives/mime.ts
1183
+ function guessMimeType(name) {
1184
+ if (!name) return "application/octet-stream";
1185
+ const ext = name.split(".").pop()?.toLowerCase() ?? "";
1186
+ const map = {
1187
+ jpg: "image/jpeg",
1188
+ jpeg: "image/jpeg",
1189
+ png: "image/png",
1190
+ gif: "image/gif",
1191
+ webp: "image/webp",
1192
+ svg: "image/svg+xml",
1193
+ pdf: "application/pdf",
1194
+ doc: "application/msword",
1195
+ docx: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1196
+ xls: "application/vnd.ms-excel",
1197
+ xlsx: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1198
+ ppt: "application/vnd.ms-powerpoint",
1199
+ pptx: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1200
+ txt: "text/plain",
1201
+ csv: "text/csv",
1202
+ json: "application/json",
1203
+ xml: "application/xml",
1204
+ zip: "application/zip",
1205
+ mp3: "audio/mpeg",
1206
+ mp4: "video/mp4",
1207
+ mov: "video/quicktime",
1208
+ avi: "video/x-msvideo",
1209
+ html: "text/html",
1210
+ css: "text/css",
1211
+ js: "application/javascript",
1212
+ ts: "application/typescript"
1213
+ };
1214
+ return map[ext] ?? "application/octet-stream";
1215
+ }
1216
+
1217
+ // src/drives/dropbox-plugin.ts
1218
+ var AUTH_URL = "https://www.dropbox.com/oauth2/authorize";
1219
+ var TOKEN_URL = "https://api.dropbox.com/oauth2/token";
1220
+ var LIST_FOLDER_URL = "https://api.dropbox.com/2/files/list_folder";
1221
+ var LIST_FOLDER_CONTINUE_URL = "https://api.dropbox.com/2/files/list_folder/continue";
1222
+ var SEARCH_URL = "https://api.dropbox.com/2/files/search_v2";
1223
+ var USER_INFO_URL = "https://api.dropbox.com/2/users/get_current_account";
1224
+ var TEMP_LINK_URL = "https://api.dropbox.com/2/files/get_temporary_link";
1225
+ var OAUTH_SCOPES = "files.metadata.read files.content.read files.content.write account_info.read";
1226
+ function mapEntry(entry) {
1227
+ const tag = entry[".tag"];
1228
+ const isFolder = tag === "folder";
1229
+ return {
1230
+ id: entry.id ?? "",
1231
+ name: entry.name ?? "",
1232
+ path: entry.path_display ?? "",
1233
+ size: isFolder ? 0 : entry.size ?? 0,
1234
+ mimeType: isFolder ? "folder" : guessMimeType(entry.name),
1235
+ isFolder,
1236
+ thumbnail: void 0,
1237
+ modifiedAt: entry.server_modified ?? void 0
1238
+ };
1239
+ }
1240
+ var DropboxPlugin = class extends PopupOAuthPlugin {
1241
+ constructor() {
1242
+ super(...arguments);
1243
+ this.spec = {
1244
+ id: "dropbox",
1245
+ displayName: "Dropbox",
1246
+ eventPrefix: "dropbox",
1247
+ popupName: "UpupDropboxAuth",
1248
+ authUrl: AUTH_URL,
1249
+ tokenUrl: TOKEN_URL,
1250
+ redirectPath: "/dp_redirect",
1251
+ storageKeys: {
1252
+ access: "upup_dropbox_access_token",
1253
+ refresh: "upup_dropbox_refresh_token",
1254
+ expiry: "upup_dropbox_token_expiry"
1255
+ },
1256
+ scopes: OAUTH_SCOPES,
1257
+ authParams: { token_access_type: "offline" }
1258
+ };
1259
+ }
1260
+ configure(config) {
1261
+ return super.configure(config);
1262
+ }
1263
+ getConfig() {
1264
+ return this.config;
1265
+ }
1266
+ // ── File operations: list folder ──
1267
+ async loadFiles(path = "") {
1268
+ this.setState("browsing");
1269
+ try {
1270
+ const res = await this.apiRequest(LIST_FOLDER_URL, {
1271
+ method: "POST",
1272
+ headers: { "Content-Type": "application/json" },
1273
+ body: JSON.stringify({
1274
+ path,
1275
+ recursive: false,
1276
+ include_media_info: true,
1277
+ include_deleted: false,
1278
+ include_has_explicit_shared_members: false
1279
+ })
1280
+ });
1281
+ const data = await res.json();
1282
+ const files = (data.entries ?? []).map(mapEntry);
1283
+ this.setState("authenticated");
1284
+ this.emitter?.emit("dropbox:files-loaded", {
1285
+ files,
1286
+ path,
1287
+ hasMore: !!data.has_more,
1288
+ cursor: data.cursor
1289
+ });
1290
+ return {
1291
+ files,
1292
+ hasMore: !!data.has_more,
1293
+ ...data.cursor !== void 0 ? { cursor: data.cursor } : {}
1294
+ };
1295
+ } catch (err) {
1296
+ this.setState("authenticated");
1297
+ this.emitter?.emit("dropbox:error", {
1298
+ error: err instanceof Error ? err : new Error(String(err)),
1299
+ action: "loadFiles"
1300
+ });
1301
+ throw err;
1302
+ }
1303
+ }
1304
+ // ── File operations: continue listing (pagination) ──
1305
+ async loadMoreFiles(cursor) {
1306
+ try {
1307
+ const res = await this.apiRequest(LIST_FOLDER_CONTINUE_URL, {
1308
+ method: "POST",
1309
+ headers: { "Content-Type": "application/json" },
1310
+ body: JSON.stringify({ cursor })
1311
+ });
1312
+ const data = await res.json();
1313
+ const files = (data.entries ?? []).map(mapEntry);
1314
+ return {
1315
+ files,
1316
+ hasMore: !!data.has_more,
1317
+ ...data.cursor !== void 0 ? { cursor: data.cursor } : {}
1318
+ };
1319
+ } catch (err) {
1320
+ this.emitter?.emit("dropbox:error", {
1321
+ error: err instanceof Error ? err : new Error(String(err)),
1322
+ action: "loadMoreFiles"
1323
+ });
1324
+ throw err;
1325
+ }
1326
+ }
1327
+ // ── File operations: load all files in folder recursively ──
1328
+ async loadAllFilesInFolder(folderPath) {
1329
+ const allFiles = [];
1330
+ try {
1331
+ const initialRes = await this.apiRequest(LIST_FOLDER_URL, {
1332
+ method: "POST",
1333
+ headers: { "Content-Type": "application/json" },
1334
+ body: JSON.stringify({
1335
+ path: folderPath,
1336
+ recursive: true,
1337
+ include_media_info: true
1338
+ })
1339
+ });
1340
+ let data = await initialRes.json();
1341
+ const entries = data.entries ?? [];
1342
+ while (data.has_more) {
1343
+ const contRes = await this.apiRequest(
1344
+ LIST_FOLDER_CONTINUE_URL,
1345
+ {
1346
+ method: "POST",
1347
+ headers: { "Content-Type": "application/json" },
1348
+ body: JSON.stringify({ cursor: data.cursor })
1349
+ }
1350
+ );
1351
+ data = await contRes.json();
1352
+ entries.push(...data.entries ?? []);
1353
+ }
1354
+ for (const entry of entries) {
1355
+ const file = mapEntry(entry);
1356
+ if (!file.isFolder) allFiles.push(file);
1357
+ }
1358
+ return allFiles;
1359
+ } catch (err) {
1360
+ this.emitter?.emit("dropbox:error", {
1361
+ error: err instanceof Error ? err : new Error(String(err)),
1362
+ action: "loadAllFilesInFolder"
1363
+ });
1364
+ throw err;
1365
+ }
1366
+ }
1367
+ // ── File operations: download files ──
1368
+ async downloadFiles(driveFiles) {
1369
+ const results = [];
1370
+ for (const driveFile of driveFiles) {
1371
+ if (driveFile.isFolder) continue;
1372
+ try {
1373
+ const file = await this.downloadSingleFile(driveFile);
1374
+ if (file) {
1375
+ results.push(file);
1376
+ }
1377
+ } catch (err) {
1378
+ this.emitter?.emit("dropbox:error", {
1379
+ error: err instanceof Error ? err : new Error(String(err)),
1380
+ action: "downloadFiles"
1381
+ });
1382
+ }
1383
+ }
1384
+ return results;
1385
+ }
1386
+ // ── File operations: search ──
1387
+ async searchFiles(query, path) {
1388
+ try {
1389
+ const body = {
1390
+ query,
1391
+ options: {
1392
+ max_results: 100,
1393
+ file_status: { ".tag": "active" }
1394
+ }
1395
+ };
1396
+ if (path) {
1397
+ body.options = {
1398
+ ...body.options,
1399
+ path
1400
+ };
1401
+ }
1402
+ const res = await this.apiRequest(SEARCH_URL, {
1403
+ method: "POST",
1404
+ headers: { "Content-Type": "application/json" },
1405
+ body: JSON.stringify(body)
1406
+ });
1407
+ const data = await res.json();
1408
+ const matches = data.matches ?? [];
1409
+ return matches.map((m) => mapEntry(m.metadata.metadata));
1410
+ } catch (err) {
1411
+ this.emitter?.emit("dropbox:error", {
1412
+ error: err instanceof Error ? err : new Error(String(err)),
1413
+ action: "searchFiles"
1414
+ });
1415
+ throw err;
1416
+ }
1417
+ }
1418
+ // ── Provider hooks required by the base ──
1419
+ mapEntry(entry) {
1420
+ return mapEntry(entry);
1421
+ }
1422
+ // ── Private: download a single file via temporary link ──
1423
+ async downloadSingleFile(driveFile) {
1424
+ const linkRes = await this.apiRequest(TEMP_LINK_URL, {
1425
+ method: "POST",
1426
+ headers: { "Content-Type": "application/json" },
1427
+ body: JSON.stringify({ path: driveFile.path })
1428
+ });
1429
+ const linkData = await linkRes.json();
1430
+ const downloadLink = linkData.link;
1431
+ if (!downloadLink) {
1432
+ throw new UpupNetworkError(
1433
+ `No download link returned for ${driveFile.name}`
1434
+ );
1435
+ }
1436
+ const downloadRes = await fetch(downloadLink, { method: "GET" });
1437
+ if (!downloadRes.ok) {
1438
+ throw new UpupNetworkError(
1439
+ `Download failed (${downloadRes.status}) for ${driveFile.name}`,
1440
+ downloadRes.status
1441
+ );
1442
+ }
1443
+ const blob = await downloadRes.blob();
1444
+ return new File([blob], driveFile.name, {
1445
+ type: blob.type || driveFile.mimeType || "application/octet-stream"
1446
+ });
1447
+ }
1448
+ // ── Private: fetch user profile ──
1449
+ async fetchUserProfile() {
1450
+ const res = await this.apiRequest(USER_INFO_URL, {
1451
+ method: "POST",
1452
+ headers: { "Content-Type": "application/json" },
1453
+ body: null
1454
+ });
1455
+ const data = await res.json();
1456
+ return {
1457
+ name: data.name?.display_name ?? "",
1458
+ email: data.email ?? ""
1459
+ };
1460
+ }
1461
+ };
1462
+
1463
+ // src/drives/google-drive-plugin.ts
1464
+ var SK_ACCESS = "upup_gdrive_access_token";
1465
+ var SK_EXPIRY = "upup_gdrive_token_expiry";
1466
+ var FILES_URL = "https://www.googleapis.com/drive/v3/files";
1467
+ var USER_INFO_URL2 = "https://www.googleapis.com/oauth2/v3/userinfo";
1468
+ var WORKSPACE_EXPORT_MAP = {
1469
+ "application/vnd.google-apps.document": {
1470
+ exportMime: "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
1471
+ ext: "docx",
1472
+ docType: "document"
1473
+ },
1474
+ "application/vnd.google-apps.spreadsheet": {
1475
+ exportMime: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
1476
+ ext: "xlsx",
1477
+ docType: "spreadsheets"
1478
+ },
1479
+ "application/vnd.google-apps.presentation": {
1480
+ exportMime: "application/vnd.openxmlformats-officedocument.presentationml.presentation",
1481
+ ext: "pptx",
1482
+ docType: "presentation"
1483
+ },
1484
+ "application/vnd.google-apps.drawing": {
1485
+ exportMime: "image/png",
1486
+ ext: "png",
1487
+ docType: "drawings"
1488
+ }
1489
+ };
1490
+ var FORMAT_MAP = {
1491
+ docx: "docx",
1492
+ xlsx: "xlsx",
1493
+ pptx: "pptx",
1494
+ png: "png"
1495
+ };
1496
+ function isWorkspaceFile(mimeType) {
1497
+ return mimeType in WORKSPACE_EXPORT_MAP;
1498
+ }
1499
+ function getExportUrl(fileId, mimeType) {
1500
+ const mapping = WORKSPACE_EXPORT_MAP[mimeType];
1501
+ if (!mapping) return null;
1502
+ const format = FORMAT_MAP[mapping.ext];
1503
+ return `https://docs.google.com/${mapping.docType}/d/${fileId}/export?format=${format}`;
1504
+ }
1505
+ function mapGoogleEntry(entry) {
1506
+ const mimeType = entry.mimeType ?? "";
1507
+ const isFolder = mimeType === "application/vnd.google-apps.folder";
1508
+ return {
1509
+ id: entry.id ?? "",
1510
+ name: entry.name ?? "",
1511
+ path: "",
1512
+ // Google Drive doesn't return a path, uses parent IDs
1513
+ size: isFolder ? 0 : typeof entry.size === "number" ? entry.size : typeof entry.size === "string" ? parseInt(entry.size, 10) || 0 : 0,
1514
+ mimeType: isFolder ? "folder" : mimeType,
1515
+ isFolder,
1516
+ thumbnail: entry.thumbnailLink ?? void 0,
1517
+ modifiedAt: void 0
1518
+ };
1519
+ }
1520
+ var GoogleDrivePlugin = class {
1521
+ constructor() {
1522
+ this.id = "google-drive";
1523
+ this.name = "google-drive";
1524
+ this.emitter = null;
1525
+ this.config = {
1526
+ apiKey: "",
1527
+ appId: "",
1528
+ clientId: ""
1529
+ };
1530
+ this.accessToken = null;
1531
+ this.tokenExpiry = 0;
1532
+ this.state = "idle";
1533
+ }
1534
+ // ── Plugin lifecycle ──
1535
+ configure(config) {
1536
+ this.config = config;
1537
+ return this;
1538
+ }
1539
+ getConfig() {
1540
+ return this.config;
1541
+ }
1542
+ init(emitter) {
1543
+ this.emitter = emitter;
1544
+ }
1545
+ destroy() {
1546
+ this.emitter = null;
1547
+ }
1548
+ // ── State management ──
1549
+ getState() {
1550
+ return this.state;
1551
+ }
1552
+ setState(newState) {
1553
+ this.state = newState;
1554
+ this.emitter?.emit("google-drive:state-change", { state: newState });
1555
+ }
1556
+ // ── Auth: accept token from GIS popup (called by React thin hook) ──
1557
+ setAccessToken(token, expiresIn) {
1558
+ const expiry = expiresIn ? Date.now() + expiresIn * 1e3 : 0;
1559
+ this.accessToken = token;
1560
+ this.tokenExpiry = expiry;
1561
+ storageSet(SK_ACCESS, token);
1562
+ if (expiry) storageSet(SK_EXPIRY, String(expiry));
1563
+ this.setState("authenticated");
1564
+ }
1565
+ // ── Auth: authenticate with token + fetch user info ──
1566
+ async authenticate(token, expiresIn) {
1567
+ this.setState("authenticating");
1568
+ try {
1569
+ this.setAccessToken(token, expiresIn);
1570
+ let user;
1571
+ try {
1572
+ user = await this.getUserInfo();
1573
+ } catch {
1574
+ }
1575
+ this.setState("authenticated");
1576
+ this.emitter?.emit("google-drive:authenticated", { user });
1577
+ } catch (err) {
1578
+ this.setState("idle");
1579
+ this.emitter?.emit("google-drive:error", {
1580
+ error: err instanceof Error ? err : new Error(String(err)),
1581
+ action: "authenticate"
1582
+ });
1583
+ throw err;
1584
+ }
1585
+ }
1586
+ // ── Auth: sign out ──
1587
+ signOut() {
1588
+ this.clearTokens();
1589
+ this.setState("idle");
1590
+ this.emitter?.emit("google-drive:signed-out", {});
1591
+ }
1592
+ // ── Auth: restore session from sessionStorage ──
1593
+ restoreSession() {
1594
+ const token = storageGet(SK_ACCESS);
1595
+ const expiry = storageGet(SK_EXPIRY);
1596
+ if (!token) return false;
1597
+ this.accessToken = token;
1598
+ this.tokenExpiry = expiry ? parseInt(expiry, 10) : 0;
1599
+ if (this.tokenExpiry > 0 && Date.now() > this.tokenExpiry) {
1600
+ this.clearTokens();
1601
+ this.emitter?.emit("google-drive:session-expired", {});
1602
+ this.setState("session-expired");
1603
+ return false;
1604
+ }
1605
+ this.setState("authenticated");
1606
+ return true;
1607
+ }
1608
+ // ── Auth helpers ──
1609
+ isAuthenticated() {
1610
+ return this.state === "authenticated" && this.accessToken !== null;
1611
+ }
1612
+ getAccessToken() {
1613
+ return this.accessToken;
1614
+ }
1615
+ // ── User info ──
1616
+ async getUserInfo() {
1617
+ const res = await this.apiRequest(USER_INFO_URL2, { method: "GET" });
1618
+ const data = await res.json();
1619
+ return {
1620
+ name: data.name ?? "",
1621
+ email: data.email ?? "",
1622
+ ...data.picture !== void 0 ? { picture: data.picture } : {}
1623
+ };
1624
+ }
1625
+ // ── File operations: list files ──
1626
+ async loadFiles(folderId) {
1627
+ this.setState("browsing");
1628
+ try {
1629
+ const parentId = folderId || "root";
1630
+ const q = `'${parentId}' in parents and trashed = false`;
1631
+ const params = new URLSearchParams({
1632
+ q,
1633
+ fields: "nextPageToken,files(fileExtension,id,mimeType,name,parents,size,thumbnailLink)",
1634
+ key: this.config.apiKey,
1635
+ pageSize: "1000"
1636
+ });
1637
+ const res = await this.apiRequest(
1638
+ `${FILES_URL}?${params.toString()}`,
1639
+ { method: "GET" }
1640
+ );
1641
+ const data = await res.json();
1642
+ const files = (data.files ?? []).map(mapGoogleEntry);
1643
+ const hasMore = !!data.nextPageToken;
1644
+ const cursor = hasMore ? JSON.stringify({
1645
+ folderId: parentId,
1646
+ pageToken: data.nextPageToken
1647
+ }) : void 0;
1648
+ this.setState("authenticated");
1649
+ this.emitter?.emit("google-drive:files-loaded", {
1650
+ files,
1651
+ folderId: parentId,
1652
+ hasMore,
1653
+ cursor
1654
+ });
1655
+ return {
1656
+ files,
1657
+ folderId: parentId,
1658
+ hasMore,
1659
+ ...cursor !== void 0 ? { cursor } : {}
1660
+ };
1661
+ } catch (err) {
1662
+ this.setState("authenticated");
1663
+ this.emitter?.emit("google-drive:error", {
1664
+ error: err instanceof Error ? err : new Error(String(err)),
1665
+ action: "loadFiles"
1666
+ });
1667
+ throw err;
1668
+ }
1669
+ }
1670
+ // ── File operations: continue listing (pagination) ──
1671
+ async loadMoreFiles(cursor) {
1672
+ try {
1673
+ const { folderId, pageToken } = JSON.parse(cursor);
1674
+ const q = `'${folderId}' in parents and trashed = false`;
1675
+ const params = new URLSearchParams({
1676
+ q,
1677
+ fields: "nextPageToken,files(fileExtension,id,mimeType,name,parents,size,thumbnailLink)",
1678
+ key: this.config.apiKey,
1679
+ pageSize: "1000",
1680
+ pageToken
1681
+ });
1682
+ const res = await this.apiRequest(
1683
+ `${FILES_URL}?${params.toString()}`,
1684
+ { method: "GET" }
1685
+ );
1686
+ const data = await res.json();
1687
+ const files = (data.files ?? []).map(mapGoogleEntry);
1688
+ const hasMore = !!data.nextPageToken;
1689
+ const nextCursor = hasMore ? JSON.stringify({ folderId, pageToken: data.nextPageToken }) : void 0;
1690
+ return {
1691
+ files,
1692
+ hasMore,
1693
+ ...nextCursor !== void 0 ? { cursor: nextCursor } : {}
1694
+ };
1695
+ } catch (err) {
1696
+ this.emitter?.emit("google-drive:error", {
1697
+ error: err instanceof Error ? err : new Error(String(err)),
1698
+ action: "loadMoreFiles"
1699
+ });
1700
+ throw err;
1701
+ }
1702
+ }
1703
+ // ── File operations: download multiple files ──
1704
+ async downloadFiles(driveFiles) {
1705
+ const results = [];
1706
+ for (const driveFile of driveFiles) {
1707
+ if (driveFile.isFolder) continue;
1708
+ try {
1709
+ const file = await this.downloadFile(driveFile);
1710
+ if (file) {
1711
+ results.push(file);
1712
+ }
1713
+ } catch (err) {
1714
+ this.emitter?.emit("google-drive:error", {
1715
+ error: err instanceof Error ? err : new Error(String(err)),
1716
+ action: "downloadFiles"
1717
+ });
1718
+ }
1719
+ }
1720
+ return results;
1721
+ }
1722
+ // ── File operations: download single file ──
1723
+ async downloadFile(driveFile) {
1724
+ if (isWorkspaceFile(driveFile.mimeType)) {
1725
+ return this.downloadWorkspaceFile(driveFile);
1726
+ }
1727
+ return this.downloadRegularFile(driveFile);
1728
+ }
1729
+ // ── Private: download regular (non-Workspace) file ──
1730
+ async downloadRegularFile(driveFile) {
1731
+ const params = new URLSearchParams({
1732
+ key: this.config.apiKey,
1733
+ alt: "media"
1734
+ });
1735
+ const res = await this.apiRequest(
1736
+ `${FILES_URL}/${driveFile.id}?${params.toString()}`,
1737
+ { method: "GET" }
1738
+ );
1739
+ const blob = await res.blob();
1740
+ return new File([blob], driveFile.name, {
1741
+ type: blob.type || driveFile.mimeType || "application/octet-stream"
1742
+ });
1743
+ }
1744
+ // ── Private: download Google Workspace file via export ──
1745
+ async downloadWorkspaceFile(driveFile) {
1746
+ const exportUrl = getExportUrl(driveFile.id, driveFile.mimeType);
1747
+ if (!exportUrl) return null;
1748
+ const mapping = WORKSPACE_EXPORT_MAP[driveFile.mimeType];
1749
+ if (!mapping) return null;
1750
+ const res = await this.apiRequest(exportUrl, { method: "GET" });
1751
+ const blob = await res.blob();
1752
+ const baseName = driveFile.name.replace(/\.[^.]+$/, "");
1753
+ const fileName = `${baseName}.${mapping.ext}`;
1754
+ return new File([blob], fileName, {
1755
+ type: mapping.exportMime
1756
+ });
1757
+ }
1758
+ // ── Private: authenticated API request ──
1759
+ async apiRequest(url, options) {
1760
+ this.ensureValidToken();
1761
+ const headers = new Headers(options.headers ?? {});
1762
+ headers.set("Authorization", `Bearer ${this.accessToken}`);
1763
+ const res = await fetch(url, { ...options, headers });
1764
+ if (res.ok) return res;
1765
+ const errorText = await res.text();
1766
+ if (res.status === 401) {
1767
+ this.emitter?.emit("google-drive:session-expired", {});
1768
+ this.clearTokens();
1769
+ this.setState("session-expired");
1770
+ }
1771
+ throw new UpupNetworkError(
1772
+ `Google Drive API error (${res.status}): ${errorText}`,
1773
+ res.status
1774
+ );
1775
+ }
1776
+ // ── Private: token management ──
1777
+ clearTokens() {
1778
+ this.accessToken = null;
1779
+ this.tokenExpiry = 0;
1780
+ storageDel(SK_ACCESS);
1781
+ storageDel(SK_EXPIRY);
1782
+ }
1783
+ ensureValidToken() {
1784
+ if (!this.accessToken) {
1785
+ throw new UpupAuthError(
1786
+ "Not authenticated \u2014 no access token",
1787
+ "Google Drive"
1788
+ );
1789
+ }
1790
+ if (this.tokenExpiry > 0 && Date.now() > this.tokenExpiry) {
1791
+ this.clearTokens();
1792
+ this.emitter?.emit("google-drive:session-expired", {});
1793
+ this.setState("session-expired");
1794
+ throw new UpupAuthError("Access token has expired", "Google Drive");
1795
+ }
1796
+ }
1797
+ };
1798
+
1799
+ // src/drives/box-plugin.ts
1800
+ var AUTH_URL2 = "https://account.box.com/api/oauth2/authorize";
1801
+ var TOKEN_URL2 = "https://api.box.com/oauth2/token";
1802
+ var USER_URL = "https://api.box.com/2.0/users/me";
1803
+ var FOLDERS_URL = "https://api.box.com/2.0/folders";
1804
+ var FILES_URL2 = "https://api.box.com/2.0/files";
1805
+ var SEARCH_URL2 = "https://api.box.com/2.0/search";
1806
+ function mapEntry2(entry) {
1807
+ const type = entry.type;
1808
+ const isFolder = type === "folder";
1809
+ return {
1810
+ id: entry.id ?? "",
1811
+ name: entry.name ?? "",
1812
+ path: entry.id ?? "",
1813
+ // Box uses IDs for navigation, not paths
1814
+ size: isFolder ? 0 : entry.size ?? 0,
1815
+ mimeType: isFolder ? "folder" : guessMimeType(entry.name),
1816
+ isFolder,
1817
+ thumbnail: void 0,
1818
+ modifiedAt: entry.modified_at ?? void 0
1819
+ };
1820
+ }
1821
+ var BoxPlugin = class extends PopupOAuthPlugin {
1822
+ constructor() {
1823
+ super(...arguments);
1824
+ this.spec = {
1825
+ id: "box",
1826
+ displayName: "Box",
1827
+ eventPrefix: "box",
1828
+ popupName: "UpupBoxAuth",
1829
+ authUrl: AUTH_URL2,
1830
+ tokenUrl: TOKEN_URL2,
1831
+ redirectPath: "/box_redirect",
1832
+ storageKeys: {
1833
+ access: "upup_box_access_token",
1834
+ refresh: "upup_box_refresh_token",
1835
+ expiry: "upup_box_token_expiry"
1836
+ }
1837
+ // Box scopes are configured on the Box app, not sent on the authorize URL.
1838
+ };
1839
+ }
1840
+ configure(config) {
1841
+ return super.configure(config);
1842
+ }
1843
+ getConfig() {
1844
+ return this.config;
1845
+ }
1846
+ // ── File operations: list folder ──
1847
+ async loadFiles(folderId = "0", offset = "0") {
1848
+ this.setState("browsing");
1849
+ try {
1850
+ const params = new URLSearchParams({
1851
+ fields: "id,name,type,size,modified_at",
1852
+ limit: "1000",
1853
+ offset
1854
+ });
1855
+ const res = await this.apiRequest(
1856
+ `${FOLDERS_URL}/${folderId}/items?${params.toString()}`,
1857
+ { method: "GET" }
1858
+ );
1859
+ const data = await res.json();
1860
+ const files = (data.entries ?? []).map(mapEntry2);
1861
+ const off = Number(data.offset ?? offset);
1862
+ const loaded = off + files.length;
1863
+ const hasMore = loaded < (data.total_count ?? loaded);
1864
+ const cursor = hasMore ? `${folderId}:${loaded}` : void 0;
1865
+ this.setState("authenticated");
1866
+ this.emitter?.emit("box:files-loaded", {
1867
+ files,
1868
+ folderId,
1869
+ hasMore,
1870
+ cursor
1871
+ });
1872
+ return {
1873
+ files,
1874
+ folderId,
1875
+ hasMore,
1876
+ ...cursor !== void 0 ? { cursor } : {}
1877
+ };
1878
+ } catch (err) {
1879
+ this.setState("authenticated");
1880
+ this.emitter?.emit("box:error", {
1881
+ error: err instanceof Error ? err : new Error(String(err)),
1882
+ action: "loadFiles"
1883
+ });
1884
+ throw err;
1885
+ }
1886
+ }
1887
+ // ── File operations: continue listing (pagination) ──
1888
+ async loadMoreFiles(cursor) {
1889
+ const [folderId, offsetStr] = cursor.split(":");
1890
+ try {
1891
+ const params = new URLSearchParams({
1892
+ fields: "id,name,type,size,modified_at",
1893
+ limit: "1000",
1894
+ offset: offsetStr ?? "0"
1895
+ });
1896
+ const res = await this.apiRequest(
1897
+ `${FOLDERS_URL}/${folderId}/items?${params.toString()}`,
1898
+ { method: "GET" }
1899
+ );
1900
+ const data = await res.json();
1901
+ const files = (data.entries ?? []).map(mapEntry2);
1902
+ const off = Number(data.offset ?? offsetStr ?? 0);
1903
+ const loaded = off + files.length;
1904
+ const hasMore = loaded < (data.total_count ?? loaded);
1905
+ const nextCursor = hasMore ? `${folderId}:${loaded}` : void 0;
1906
+ return {
1907
+ files,
1908
+ hasMore,
1909
+ ...nextCursor !== void 0 ? { cursor: nextCursor } : {}
1910
+ };
1911
+ } catch (err) {
1912
+ this.emitter?.emit("box:error", {
1913
+ error: err instanceof Error ? err : new Error(String(err)),
1914
+ action: "loadMoreFiles"
1915
+ });
1916
+ throw err;
1917
+ }
1918
+ }
1919
+ // ── File operations: download files ──
1920
+ async downloadFiles(driveFiles) {
1921
+ const results = [];
1922
+ for (const driveFile of driveFiles) {
1923
+ if (driveFile.isFolder) continue;
1924
+ try {
1925
+ const file = await this.downloadSingleFile(driveFile);
1926
+ if (file) {
1927
+ results.push(file);
1928
+ }
1929
+ } catch (err) {
1930
+ this.emitter?.emit("box:error", {
1931
+ error: err instanceof Error ? err : new Error(String(err)),
1932
+ action: "downloadFiles"
1933
+ });
1934
+ }
1935
+ }
1936
+ return results;
1937
+ }
1938
+ // ── File operations: load all files in folder recursively ──
1939
+ async loadAllFilesInFolder(folderId) {
1940
+ const allFiles = [];
1941
+ try {
1942
+ const { files } = await this.loadFiles(folderId);
1943
+ for (const file of files) {
1944
+ if (file.isFolder) {
1945
+ const nested = await this.loadAllFilesInFolder(file.id);
1946
+ allFiles.push(...nested);
1947
+ } else {
1948
+ allFiles.push(file);
1949
+ }
1950
+ }
1951
+ return allFiles;
1952
+ } catch (err) {
1953
+ this.emitter?.emit("box:error", {
1954
+ error: err instanceof Error ? err : new Error(String(err)),
1955
+ action: "loadAllFilesInFolder"
1956
+ });
1957
+ throw err;
1958
+ }
1959
+ }
1960
+ // ── File operations: search ──
1961
+ async searchFiles(query) {
1962
+ try {
1963
+ const params = new URLSearchParams({
1964
+ query,
1965
+ fields: "id,name,type,size,modified_at",
1966
+ limit: "100"
1967
+ });
1968
+ const res = await this.apiRequest(
1969
+ `${SEARCH_URL2}?${params.toString()}`,
1970
+ { method: "GET" }
1971
+ );
1972
+ const data = await res.json();
1973
+ const entries = data.entries ?? [];
1974
+ return entries.map(mapEntry2);
1975
+ } catch (err) {
1976
+ this.emitter?.emit("box:error", {
1977
+ error: err instanceof Error ? err : new Error(String(err)),
1978
+ action: "searchFiles"
1979
+ });
1980
+ throw err;
1981
+ }
1982
+ }
1983
+ // ── Provider hooks required by the base ──
1984
+ mapEntry(entry) {
1985
+ return mapEntry2(entry);
1986
+ }
1987
+ // ── Private: download a single file ──
1988
+ async downloadSingleFile(driveFile) {
1989
+ const res = await this.apiRequest(
1990
+ `${FILES_URL2}/${driveFile.id}/content`,
1991
+ { method: "GET" }
1992
+ );
1993
+ const blob = await res.blob();
1994
+ return new File([blob], driveFile.name, {
1995
+ type: blob.type || driveFile.mimeType || "application/octet-stream"
1996
+ });
1997
+ }
1998
+ // ── Private: fetch user profile ──
1999
+ async fetchUserProfile() {
2000
+ const res = await this.apiRequest(USER_URL, { method: "GET" });
2001
+ const data = await res.json();
2002
+ return {
2003
+ name: data.name ?? "",
2004
+ email: data.login ?? ""
2005
+ };
2006
+ }
2007
+ };
2008
+
2009
+ // src/drives/one-drive-plugin.ts
2010
+ var AUTH_URL3 = "https://login.microsoftonline.com/common/oauth2/v2.0/authorize";
2011
+ var TOKEN_URL3 = "https://login.microsoftonline.com/common/oauth2/v2.0/token";
2012
+ var GRAPH_BASE = "https://graph.microsoft.com/v1.0";
2013
+ var OAUTH_SCOPES2 = "user.read files.readwrite.all offline_access";
2014
+ var FILE_SELECT = "id,name,folder,file,size,@microsoft.graph.downloadUrl";
2015
+ var FILE_EXPAND = "thumbnails";
2016
+ function mapGraphItem(item) {
2017
+ const isFolder = !!item.folder;
2018
+ const file = item.file;
2019
+ const mimeType = isFolder ? "folder" : file?.mimeType ?? guessMimeType(item.name);
2020
+ const thumbnails = item.thumbnails;
2021
+ let thumbnail;
2022
+ if (thumbnails && thumbnails.length > 0) {
2023
+ const thumbSet = thumbnails[0];
2024
+ const medium = thumbSet?.medium;
2025
+ thumbnail = medium?.url ?? void 0;
2026
+ }
2027
+ return {
2028
+ id: item.id ?? "",
2029
+ name: item.name ?? "",
2030
+ path: item.id ?? "",
2031
+ // OneDrive uses id-based navigation
2032
+ size: isFolder ? 0 : item.size ?? 0,
2033
+ mimeType,
2034
+ isFolder,
2035
+ thumbnail,
2036
+ modifiedAt: item.lastModifiedDateTime ?? void 0
2037
+ };
2038
+ }
2039
+ var OneDrivePlugin = class extends PopupOAuthPlugin {
2040
+ constructor() {
2041
+ super(...arguments);
2042
+ this.spec = {
2043
+ id: "one-drive",
2044
+ displayName: "OneDrive",
2045
+ eventPrefix: "one-drive",
2046
+ popupName: "UpupOneDriveAuth",
2047
+ authUrl: AUTH_URL3,
2048
+ tokenUrl: TOKEN_URL3,
2049
+ redirectPath: "/od_redirect",
2050
+ storageKeys: {
2051
+ access: "upup_one-drive_access_token",
2052
+ refresh: "upup_one-drive_refresh_token",
2053
+ expiry: "upup_one-drive_token_expiry"
2054
+ },
2055
+ scopes: OAUTH_SCOPES2,
2056
+ authParams: { response_mode: "query" }
2057
+ };
2058
+ }
2059
+ configure(config) {
2060
+ return super.configure(config);
2061
+ }
2062
+ getConfig() {
2063
+ return this.config;
2064
+ }
2065
+ // ── File operations: list files ──
2066
+ async loadFiles(folderId) {
2067
+ this.setState("browsing");
2068
+ try {
2069
+ const path = folderId ? `/me/drive/items/${folderId}/children` : "/me/drive/root/children";
2070
+ const params = new URLSearchParams({
2071
+ $select: FILE_SELECT,
2072
+ $expand: FILE_EXPAND,
2073
+ $top: "200"
2074
+ });
2075
+ const data = await this.graphRequest(`${path}?${params.toString()}`);
2076
+ const items = Array.isArray(data.value) ? data.value : [];
2077
+ const files = items.map(mapGraphItem);
2078
+ const nextLink = data["@odata.nextLink"];
2079
+ const hasMore = !!nextLink;
2080
+ this.setState("authenticated");
2081
+ this.emitter?.emit("one-drive:files-loaded", {
2082
+ files,
2083
+ folderId: folderId ?? "root",
2084
+ hasMore,
2085
+ cursor: nextLink
2086
+ });
2087
+ return {
2088
+ files,
2089
+ folderId: folderId ?? "root",
2090
+ hasMore,
2091
+ ...nextLink !== void 0 ? { cursor: nextLink } : {}
2092
+ };
2093
+ } catch (err) {
2094
+ this.setState("authenticated");
2095
+ this.emitter?.emit("one-drive:error", {
2096
+ error: err instanceof Error ? err : new Error(String(err)),
2097
+ action: "loadFiles"
2098
+ });
2099
+ throw err;
2100
+ }
2101
+ }
2102
+ // ── File operations: continue listing (pagination) ──
2103
+ async loadMoreFiles(cursor) {
2104
+ try {
2105
+ const data = await this.graphRequest(cursor);
2106
+ const items = Array.isArray(data.value) ? data.value : [];
2107
+ const files = items.map(mapGraphItem);
2108
+ const nextLink = data["@odata.nextLink"];
2109
+ return {
2110
+ files,
2111
+ hasMore: !!nextLink,
2112
+ ...nextLink !== void 0 ? { cursor: nextLink } : {}
2113
+ };
2114
+ } catch (err) {
2115
+ this.emitter?.emit("one-drive:error", {
2116
+ error: err instanceof Error ? err : new Error(String(err)),
2117
+ action: "loadMoreFiles"
2118
+ });
2119
+ throw err;
2120
+ }
2121
+ }
2122
+ // ── File operations: download files ──
2123
+ async downloadFiles(driveFiles) {
2124
+ const results = [];
2125
+ for (const driveFile of driveFiles) {
2126
+ if (driveFile.isFolder) continue;
2127
+ try {
2128
+ const file = await this.downloadSingleFile(driveFile);
2129
+ if (file) {
2130
+ results.push(file);
2131
+ }
2132
+ } catch (err) {
2133
+ this.emitter?.emit("one-drive:error", {
2134
+ error: err instanceof Error ? err : new Error(String(err)),
2135
+ action: "downloadFiles"
2136
+ });
2137
+ }
2138
+ }
2139
+ return results;
2140
+ }
2141
+ // ── File operations: load all files in folder recursively ──
2142
+ async loadAllFilesInFolder(folderId) {
2143
+ const allFiles = [];
2144
+ try {
2145
+ const { files } = await this.loadFiles(folderId);
2146
+ for (const file of files) {
2147
+ if (file.isFolder) {
2148
+ const nested = await this.loadAllFilesInFolder(file.id);
2149
+ allFiles.push(...nested);
2150
+ } else {
2151
+ allFiles.push(file);
2152
+ }
2153
+ }
2154
+ return allFiles;
2155
+ } catch (err) {
2156
+ this.emitter?.emit("one-drive:error", {
2157
+ error: err instanceof Error ? err : new Error(String(err)),
2158
+ action: "loadAllFilesInFolder"
2159
+ });
2160
+ throw err;
2161
+ }
2162
+ }
2163
+ // ── Provider hooks required by the base ──
2164
+ mapEntry(entry) {
2165
+ return mapGraphItem(entry);
2166
+ }
2167
+ // ── Private: fetch user profile (unguarded — called once tokens are set
2168
+ // but before isAuthenticated() flips, and again via the public getUserInfo) ──
2169
+ async fetchUserProfile() {
2170
+ const data = await this.graphRequest("/me");
2171
+ const displayName = data.displayName;
2172
+ const mail = data.mail;
2173
+ const userPrincipalName = data.userPrincipalName;
2174
+ return {
2175
+ name: typeof displayName === "string" ? displayName : "",
2176
+ email: typeof mail === "string" ? mail : typeof userPrincipalName === "string" ? userPrincipalName : ""
2177
+ };
2178
+ }
2179
+ // ── Private: Microsoft Graph API request (delegates auth/refresh/retry to
2180
+ // the base apiRequest; returns parsed JSON, tolerating absolute URLs) ──
2181
+ async graphRequest(path, options = {}) {
2182
+ const url = path.startsWith("http") ? path : `${GRAPH_BASE}${path}`;
2183
+ const res = await this.apiRequest(url, options);
2184
+ return await res.json();
2185
+ }
2186
+ // ── Private: download a single file ──
2187
+ async downloadSingleFile(driveFile) {
2188
+ const itemData = await this.graphRequest(
2189
+ `/me/drive/items/${driveFile.id}?select=@microsoft.graph.downloadUrl`
2190
+ );
2191
+ const downloadUrl = itemData["@microsoft.graph.downloadUrl"] ?? itemData["@content.downloadUrl"];
2192
+ if (!downloadUrl) {
2193
+ throw new UpupNetworkError(
2194
+ `No download URL available for ${driveFile.name}`
2195
+ );
2196
+ }
2197
+ const downloadRes = await fetch(downloadUrl, { method: "GET" });
2198
+ if (!downloadRes.ok) {
2199
+ throw new UpupNetworkError(
2200
+ `Download failed (${downloadRes.status}) for ${driveFile.name}`,
2201
+ downloadRes.status
2202
+ );
2203
+ }
2204
+ const blob = await downloadRes.blob();
2205
+ return new File([blob], driveFile.name, {
2206
+ type: blob.type || driveFile.mimeType || "application/octet-stream"
2207
+ });
2208
+ }
2209
+ };
2210
+
2211
+ // src/utils/accept-presets.ts
2212
+ var ACCEPT_PRESETS = {
2213
+ images: {
2214
+ label: "Images",
2215
+ accept: "image/*"
2216
+ },
2217
+ video: {
2218
+ label: "Video",
2219
+ accept: "video/*"
2220
+ },
2221
+ audio: {
2222
+ label: "Audio",
2223
+ accept: "audio/*"
2224
+ },
2225
+ documents: {
2226
+ label: "Documents",
2227
+ accept: "application/pdf,application/msword,application/vnd.openxmlformats-officedocument.wordprocessingml.document,application/vnd.oasis.opendocument.text,text/plain,text/rtf"
2228
+ },
2229
+ spreadsheets: {
2230
+ label: "Spreadsheets",
2231
+ accept: "application/vnd.ms-excel,application/vnd.openxmlformats-officedocument.spreadsheetml.sheet,text/csv,application/vnd.oasis.opendocument.spreadsheet,.tsv"
2232
+ },
2233
+ presentations: {
2234
+ label: "Presentations",
2235
+ accept: "application/vnd.ms-powerpoint,application/vnd.openxmlformats-officedocument.presentationml.presentation,application/vnd.oasis.opendocument.presentation,.key"
2236
+ },
2237
+ archives: {
2238
+ label: "Archives",
2239
+ accept: "application/zip,application/x-rar-compressed,application/x-7z-compressed,application/x-tar,application/gzip,.bz2"
2240
+ },
2241
+ code: {
2242
+ label: "Code",
2243
+ accept: "text/javascript,application/json,text/html,text/css,text/xml,text/yaml,.ts,.tsx,.jsx,.py,.rb,.go,.rs,.java,.cpp,.c,.h,.sh,.sql"
2244
+ },
2245
+ fonts: {
2246
+ label: "Fonts",
2247
+ accept: "font/ttf,font/otf,font/woff,font/woff2,.ttf,.otf,.woff,.woff2"
2248
+ },
2249
+ "3d": {
2250
+ label: "3D",
2251
+ accept: ".obj,.stl,.gltf,.glb,.fbx,.usdz"
2252
+ },
2253
+ design: {
2254
+ label: "Design",
2255
+ accept: "image/svg+xml,.psd,.ai,.sketch,.fig,.xd"
2256
+ },
2257
+ ebooks: {
2258
+ label: "Ebooks",
2259
+ accept: "application/epub+zip,application/pdf,.mobi,.azw"
2260
+ },
2261
+ photography: {
2262
+ label: "Photography",
2263
+ accept: "image/*,.cr2,.cr3,.nef,.arw,.dng,.orf,.rw2,.raf,.raw"
2264
+ },
2265
+ animation: {
2266
+ label: "Animation",
2267
+ accept: "image/gif,image/apng,image/webp,.lottie,.rive"
2268
+ },
2269
+ ar: {
2270
+ label: "AR",
2271
+ accept: ".usdz,.reality,.glb,.gltf"
2272
+ },
2273
+ vector: {
2274
+ label: "Vector",
2275
+ accept: "image/svg+xml,.eps,.ai"
2276
+ },
2277
+ cad: {
2278
+ label: "CAD",
2279
+ accept: ".dwg,.dxf,.step,.stp,.iges,.igs"
2280
+ },
2281
+ gis: {
2282
+ label: "GIS",
2283
+ accept: "application/geo+json,.kml,.kmz,.shp,.gpx,.geojson"
2284
+ },
2285
+ data: {
2286
+ label: "Data",
2287
+ accept: "application/json,text/xml,text/csv,text/yaml,.toml,.parquet,.jsonl,.ndjson"
2288
+ },
2289
+ markup: {
2290
+ label: "Markup",
2291
+ accept: "text/markdown,.md,.rst,.tex,.adoc,.latex"
2292
+ },
2293
+ subtitles: {
2294
+ label: "Subtitles",
2295
+ accept: ".srt,.vtt,.ass,.sub,.ssa"
2296
+ },
2297
+ email: {
2298
+ label: "Email",
2299
+ accept: ".eml,.msg,.mbox"
2300
+ },
2301
+ calendar: {
2302
+ label: "Calendar",
2303
+ accept: "text/calendar,.ics,.ical"
2304
+ },
2305
+ contacts: {
2306
+ label: "Contacts",
2307
+ accept: "text/vcard,.vcf"
2308
+ },
2309
+ disk: {
2310
+ label: "Disk",
2311
+ accept: ".iso,.img,.dmg,.vhd,.vhdx"
2312
+ },
2313
+ ml: {
2314
+ label: "ML",
2315
+ accept: ".onnx,.h5,.pb,.pt,.safetensors,.pkl"
2316
+ },
2317
+ database: {
2318
+ label: "Database",
2319
+ accept: ".sql,.sqlite,.db,.mdb,.accdb"
2320
+ },
2321
+ certificates: {
2322
+ label: "Certificates",
2323
+ accept: ".pem,.crt,.cer,.p12,.pfx,.key"
2324
+ },
2325
+ firmware: {
2326
+ label: "Firmware",
2327
+ accept: ".bin,.hex,.elf,.srec"
2328
+ },
2329
+ executable: {
2330
+ label: "Executable",
2331
+ accept: ".exe,.msi,.dmg,.apk,.deb,.rpm,.appimage"
2332
+ },
2333
+ config: {
2334
+ label: "Config",
2335
+ accept: ".ini,.cfg,.conf,.env,.toml,.properties,.yaml,.yml"
2336
+ },
2337
+ financial: {
2338
+ label: "Financial",
2339
+ accept: ".ofx,.qfx,.qbo,.qif"
2340
+ },
2341
+ scientific: {
2342
+ label: "Scientific",
2343
+ accept: ".fits,.hdf5,.nc,.mat,.netcdf"
2344
+ },
2345
+ medical: {
2346
+ label: "Medical",
2347
+ accept: "application/dicom,.dcm,.nii,.nii.gz"
2348
+ }
2349
+ };
2350
+ function resolveAccept(input) {
2351
+ const raw = Array.isArray(input) ? input.join(",") : input;
2352
+ return raw.split(",").map((token) => {
2353
+ const trimmed = token.trim();
2354
+ const key = trimmed.toLowerCase();
2355
+ return ACCEPT_PRESETS[key]?.accept ?? trimmed;
2356
+ }).join(",");
2357
+ }
2358
+
2359
+ export { ACCEPT_PRESETS, BoxPlugin, CrashRecoveryManager, DropboxPlugin, EventEmitter, FileManager, GoogleDrivePlugin, IndexedDBStorage, OneDrivePlugin, PluginManager, PopupOAuthPlugin, UploadManager, isFileRemovalLocked, isUploadActive, isUploadIdle, resolveAccept, validateFileRestrictions };