@gentleduck/upload 0.2.6 → 0.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 (136) hide show
  1. package/README.md +170 -193
  2. package/dist/core/index.d.ts +2 -3
  3. package/dist/core/index.js +1 -1
  4. package/dist/core-BVgloTiP.js +10 -0
  5. package/dist/index-DCqyBVcx.d.ts +1441 -0
  6. package/dist/index-DsZHPmJm.d.ts +225 -0
  7. package/dist/index-KROXtLQh.d.ts +281 -0
  8. package/dist/index.d.ts +4 -5
  9. package/dist/index.js +1 -1
  10. package/dist/react/index.d.ts +2 -51
  11. package/dist/react/index.js +1 -1
  12. package/dist/react-euPrDv24.js +1 -0
  13. package/dist/strategies/index.d.ts +2 -56
  14. package/dist/strategies/index.js +1 -1
  15. package/dist/strategies-CUF0s68j.js +1 -0
  16. package/package.json +3 -2
  17. package/src/{__test__ → __tests__}/emitter.test.ts +4 -4
  18. package/src/{__test__ → __tests__}/fingerprint.test.ts +1 -1
  19. package/src/{__test__ → __tests__}/guards.test.ts +15 -5
  20. package/src/__tests__/id.test.ts +67 -0
  21. package/src/__tests__/mime-sniff.test.ts +142 -0
  22. package/src/{__test__ → __tests__}/persistence.test.ts +43 -8
  23. package/src/__tests__/sanitize-filename.test.ts +109 -0
  24. package/src/__tests__/sleep.test.ts +34 -0
  25. package/src/__tests__/static-analysis.test.ts +145 -0
  26. package/src/{__test__ → __tests__}/store-libs.test.ts +57 -3
  27. package/src/__tests__/transport-fetch.test.ts +108 -0
  28. package/src/{__test__ → __tests__}/transport.test.ts +5 -5
  29. package/src/core/__tests__/errors.test.ts +152 -0
  30. package/src/core/client.ts +3 -0
  31. package/src/core/contracts/contracts.types.ts +335 -141
  32. package/src/core/contracts/index.ts +3 -8
  33. package/src/core/contracts/transport/index.ts +3 -2
  34. package/src/core/contracts/transport/transport.fetch.ts +203 -0
  35. package/src/core/contracts/transport/transport.libs.ts +7 -8
  36. package/src/core/contracts/transport/transport.ts +91 -83
  37. package/src/core/contracts/transport/transport.types.ts +71 -35
  38. package/src/core/engine/__tests__/config-nan-bypass.test.ts +40 -0
  39. package/src/core/engine/engine.libs.ts +35 -0
  40. package/src/core/engine/engine.types.ts +367 -241
  41. package/src/core/engine/index.ts +3 -8
  42. package/src/core/engine/reducer.ts +89 -43
  43. package/src/core/engine/store/handlers/add-file.ts +39 -29
  44. package/src/core/engine/store/handlers/cancel.ts +13 -8
  45. package/src/core/engine/store/handlers/clean-up.ts +11 -8
  46. package/src/core/engine/store/handlers/create-intent.ts +82 -27
  47. package/src/core/engine/store/handlers/finalize.ts +30 -13
  48. package/src/core/engine/store/handlers/index.ts +7 -7
  49. package/src/core/engine/store/handlers/pause.ts +8 -6
  50. package/src/core/engine/store/handlers/run-upload.ts +49 -34
  51. package/src/core/engine/store/index.ts +27 -21
  52. package/src/core/engine/store/store.dispatch.ts +10 -8
  53. package/src/core/engine/store/store.libs.ts +184 -41
  54. package/src/core/engine/store/store.runtime.ts +45 -36
  55. package/src/core/engine/store/store.schedule.ts +27 -21
  56. package/src/core/engine/store/store.types.ts +177 -227
  57. package/src/core/engine/validation/__tests__/intent.test.ts +70 -0
  58. package/src/core/engine/validation/file.ts +7 -3
  59. package/src/core/engine/validation/index.ts +1 -0
  60. package/src/core/engine/validation/intent.ts +49 -27
  61. package/src/core/engine/validation/list.ts +14 -15
  62. package/src/core/engine/validation/mime.ts +39 -0
  63. package/src/core/errors.ts +271 -0
  64. package/src/core/index.ts +28 -5
  65. package/src/core/persistence/__tests__/deserialize-nan-bypass.test.ts +100 -0
  66. package/src/core/persistence/__tests__/persistence.test.ts +85 -0
  67. package/src/core/persistence/adapters.indexeddb.ts +9 -3
  68. package/src/core/persistence/adapters.local.ts +9 -3
  69. package/src/core/persistence/adapters.memory.ts +35 -13
  70. package/src/core/persistence/index.ts +5 -5
  71. package/src/core/persistence/persistence.ts +73 -60
  72. package/src/core/persistence/persistence.types.ts +140 -60
  73. package/src/core/utils/__tests__/guards.test.ts +93 -0
  74. package/src/core/utils/__tests__/url-safety.test.ts +112 -0
  75. package/src/core/utils/async.ts +7 -2
  76. package/src/core/utils/constants.ts +3 -9
  77. package/src/core/utils/emitter/emitter.ts +7 -24
  78. package/src/core/utils/emitter/emitter.types.ts +33 -16
  79. package/src/core/utils/emitter/index.ts +2 -74
  80. package/src/core/utils/fingerprint.ts +16 -5
  81. package/src/core/utils/guards.ts +91 -1
  82. package/src/core/utils/hash/__tests__/sha256.test.ts +61 -0
  83. package/src/core/utils/hash/index.ts +126 -0
  84. package/src/core/utils/hash/sha256.ts +179 -0
  85. package/src/core/utils/id.ts +33 -1
  86. package/src/core/utils/index.ts +15 -6
  87. package/src/core/utils/mime-sniff.ts +119 -0
  88. package/src/core/utils/sanitize-filename.ts +85 -0
  89. package/src/core/utils/url-safety.ts +156 -0
  90. package/src/react/__tests__/use-dropzone.test.ts +64 -0
  91. package/src/react/index.ts +5 -2
  92. package/src/react/upload-provider.tsx +68 -53
  93. package/src/react/uploader.types.ts +58 -61
  94. package/src/react/use-dropzone.tsx +226 -0
  95. package/src/react/use-uploader.tsx +98 -91
  96. package/src/strategies/_shared/__tests__/retry.test.ts +120 -0
  97. package/src/strategies/_shared/retry.ts +79 -0
  98. package/src/strategies/index.ts +9 -15
  99. package/src/strategies/multipart/__tests__/multipart.test.ts +229 -0
  100. package/src/strategies/multipart/index.ts +206 -108
  101. package/src/strategies/post/__tests__/post-url-safety.test.ts +119 -0
  102. package/src/strategies/post/index.ts +115 -33
  103. package/src/strategies/put/__tests__/put.test.ts +132 -0
  104. package/src/strategies/put/index.ts +139 -0
  105. package/src/strategies/registry/index.ts +12 -9
  106. package/src/strategies/tus/__tests__/tus.test.ts +159 -0
  107. package/src/strategies/tus/index.ts +211 -0
  108. package/dist/async-DxRWu4le.js +0 -1
  109. package/dist/core-CMkrP9DX.js +0 -2
  110. package/dist/index-C38Jmizu.d.ts +0 -1005
  111. package/dist/strategy.types-gg9ZStRe.d.ts +0 -225
  112. package/src/__test__/effect-pool.test.ts +0 -5
  113. package/src/__test__/id.test.ts +0 -14
  114. package/src/__test__/store-integration.test.ts +0 -5
  115. package/src/core/client/client.ts +0 -50
  116. package/src/core/client/client.types.ts +0 -184
  117. package/src/core/client/config.types.ts +0 -77
  118. package/src/core/client/index.ts +0 -4
  119. package/src/core/client/plugins.types.ts +0 -46
  120. package/src/core/client/retry.types.ts +0 -6
  121. package/src/core/client/uploadClient.ts +0 -19
  122. package/src/core/contracts/backendApi.ts +0 -62
  123. package/src/core/contracts/cursor.types.ts +0 -23
  124. package/src/core/contracts/errors.types.ts +0 -67
  125. package/src/core/contracts/fingerprint.types.ts +0 -18
  126. package/src/core/contracts/intent.types.ts +0 -39
  127. package/src/core/contracts/result.types.ts +0 -11
  128. package/src/core/contracts/strategy.types.ts +0 -45
  129. package/src/core/engine/commands.types.ts +0 -17
  130. package/src/core/engine/event-map.types.ts +0 -45
  131. package/src/core/engine/internal-events.types.ts +0 -202
  132. package/src/core/engine/outcome.types.ts +0 -9
  133. package/src/core/engine/progress.types.ts +0 -15
  134. package/src/core/engine/store/handlers.ts +0 -819
  135. package/src/core/engine/validation.ts +0 -216
  136. /package/src/{__test__ → __tests__}/transport-libs.test.ts +0 -0
@@ -1,216 +0,0 @@
1
- import type { Client } from '../client'
2
- import type { Contracts } from '../contracts'
3
- import { isRecord } from '../utils/guards'
4
-
5
- // ============================================================================
6
- // validateFile -- single-file rule check
7
- // ============================================================================
8
-
9
- /**
10
- * Validate one file against `config.validation[purpose]` rules.
11
- *
12
- * Empty files are always rejected.
13
- *
14
- * @param file File to validate.
15
- * @param purpose Purpose key used to look up rules.
16
- * @param config Resolved upload config.
17
- * @template P Purpose discriminator string.
18
- * @returns `null` when valid, otherwise a {@link Contracts.RejectReason}.
19
- * @author wildduck2 <https://github.com/wildduck2>
20
- */
21
- export function validateFile<P extends string>(
22
- file: File,
23
- purpose: P,
24
- config: Client.IUploadConfig<P>,
25
- ): Contracts.RejectReason | null {
26
- if (file.size === 0) return { code: 'empty_file' }
27
-
28
- const rules = config.validation?.[purpose]
29
- if (!rules) return null
30
-
31
- if (rules.maxSizeBytes !== undefined && file.size > rules.maxSizeBytes) {
32
- return { code: 'file_too_large', maxBytes: rules.maxSizeBytes, size: file.size }
33
- }
34
-
35
- if (rules.minSizeBytes !== undefined && file.size < rules.minSizeBytes) {
36
- return { code: 'empty_file' }
37
- }
38
-
39
- if (rules.allowedTypes || rules.allowedExtensions) {
40
- const allowed = rules.allowedTypes || []
41
- const extensions = rules.allowedExtensions || []
42
-
43
- const fileExt = file.name.split('.').pop()?.toLowerCase()
44
- const mimeType = (file.type || '').toLowerCase()
45
-
46
- const typeMatches =
47
- allowed.length === 0
48
- ? false
49
- : allowed.some((type) => {
50
- if (type.endsWith('/*')) {
51
- const prefix = type.slice(0, -2).toLowerCase()
52
- return mimeType.startsWith(`${prefix}/`)
53
- }
54
- return mimeType === type.toLowerCase()
55
- })
56
-
57
- const extMatches =
58
- fileExt && extensions.length > 0 ? extensions.some((ext) => ext.toLowerCase() === fileExt) : false
59
-
60
- const hasTypeRules = allowed.length > 0
61
- const hasExtRules = extensions.length > 0
62
-
63
- // When both rule kinds are present, either match is enough; otherwise the
64
- // present one must match.
65
- const ok = (hasTypeRules ? typeMatches : true) && (hasExtRules ? extMatches : true)
66
- const okEither = hasTypeRules && hasExtRules ? typeMatches || extMatches : ok
67
-
68
- if (!okEither) {
69
- return {
70
- code: 'type_not_allowed',
71
- allowed: [...allowed, ...extensions],
72
- got: file.type || file.name,
73
- }
74
- }
75
- }
76
-
77
- return null
78
- }
79
-
80
- // ============================================================================
81
- // validateFileList -- batch validation with maxFiles cap
82
- // ============================================================================
83
-
84
- /**
85
- * Validate a batch of files and split into accepted vs rejected.
86
- *
87
- * Enforces `maxFiles` against the existing count plus the running additions
88
- * so a single `addFiles` call cannot overshoot.
89
- *
90
- * @param files Files to validate.
91
- * @param purpose Purpose key used to look up rules.
92
- * @param config Resolved upload config.
93
- * @param existingCount Items already in the store for this purpose.
94
- * @template P Purpose discriminator string.
95
- * @returns Object with `valid` files and `rejected` files plus reasons.
96
- * @author wildduck2 <https://github.com/wildduck2>
97
- */
98
- export function validateFileList<P extends string>(
99
- files: File[],
100
- purpose: P,
101
- config: Client.IUploadConfig<P>,
102
- existingCount: number = 0,
103
- ): { valid: File[]; rejected: Array<{ file: File; reason: Contracts.RejectReason }> } {
104
- const rules = config.validation?.[purpose]
105
- const valid: File[] = []
106
- const rejected: Array<{ file: File; reason: Contracts.RejectReason }> = []
107
-
108
- const maxFiles = rules?.maxFiles
109
- let remaining = maxFiles !== undefined ? Math.max(0, maxFiles - existingCount) : Number.POSITIVE_INFINITY
110
-
111
- for (const file of files) {
112
- if (remaining <= 0) {
113
- const limit = maxFiles ?? 0
114
- rejected.push({ file, reason: { code: 'too_many_files', max: limit } })
115
- continue
116
- }
117
-
118
- const reason = validateFile(file, purpose, config)
119
- if (reason) {
120
- rejected.push({ file, reason })
121
- continue
122
- }
123
-
124
- valid.push(file)
125
- remaining -= 1
126
- }
127
-
128
- return { valid, rejected }
129
- }
130
-
131
- // ============================================================================
132
- // validateIntent -- backend-intent shape check
133
- // ============================================================================
134
-
135
- /**
136
- * Validate an intent payload returned by the backend before the engine
137
- * commits to it.
138
- *
139
- * Rejects malformed shapes, protocol-foreign URLs, and cross-field
140
- * inconsistencies (e.g. `partCount * partSize` cannot cover the file).
141
- *
142
- * @param intent Raw intent value from `createIntent`.
143
- * @param strategy Expected `strategy` discriminator.
144
- * @param fileSize File byte length; enables multipart cross-checks.
145
- * @returns `Error` describing the failure, or `null` if valid.
146
- * @author wildduck2 <https://github.com/wildduck2>
147
- */
148
- export function validateIntent(intent: unknown, strategy: string, fileSize?: number): Error | null {
149
- if (!isRecord(intent)) {
150
- return new Error('Invalid intent: must be an object')
151
- }
152
-
153
- if (typeof intent.strategy !== 'string') {
154
- return new Error('Invalid intent: missing or invalid strategy field')
155
- }
156
-
157
- if (intent.strategy !== strategy) {
158
- return new Error(`Invalid intent: strategy mismatch (expected ${strategy}, got ${intent.strategy})`)
159
- }
160
-
161
- if (typeof intent.fileId !== 'string' || !intent.fileId) {
162
- return new Error('Invalid intent: missing or invalid fileId')
163
- }
164
-
165
- if (strategy === 'post') {
166
- if (typeof intent.url !== 'string' || !intent.url) {
167
- return new Error('Invalid post intent: missing or invalid url')
168
- }
169
- try {
170
- const url = new URL(intent.url)
171
- if (!['http:', 'https:'].includes(url.protocol)) {
172
- return new Error('Invalid post intent: url must use http or https protocol')
173
- }
174
- } catch {
175
- return new Error('Invalid post intent: url is not a valid URL')
176
- }
177
- if (!intent.fields || typeof intent.fields !== 'object') {
178
- return new Error('Invalid post intent: missing or invalid fields')
179
- }
180
- } else if (strategy === 'multipart') {
181
- if (typeof intent.uploadId !== 'string' || !intent.uploadId) {
182
- return new Error('Invalid multipart intent: missing or invalid uploadId')
183
- }
184
- if (typeof intent.partSize !== 'number' || intent.partSize <= 0) {
185
- return new Error('Invalid multipart intent: missing or invalid partSize')
186
- }
187
- if ('partCount' in intent && intent.partCount !== undefined) {
188
- if (typeof intent.partCount !== 'number' || !Number.isFinite(intent.partCount) || intent.partCount <= 0) {
189
- return new Error('Invalid multipart intent: partCount must be a positive number')
190
- }
191
- }
192
- if (typeof fileSize === 'number' && typeof intent.partCount === 'number' && intent.partCount > 0) {
193
- const partSize = intent.partSize
194
- const partCount = intent.partCount
195
- const maxBytes = partCount * partSize
196
- const minBytes = (partCount - 1) * partSize
197
- if (maxBytes < fileSize) {
198
- return new Error(
199
- `Invalid multipart intent: partCount * partSize (${maxBytes}) is smaller than file size (${fileSize})`,
200
- )
201
- }
202
- if (minBytes >= fileSize && partCount > 1) {
203
- return new Error(
204
- `Invalid multipart intent: partCount (${partCount}) exceeds what file size (${fileSize}) needs at partSize ${partSize}`,
205
- )
206
- }
207
- }
208
- if ('parts' in intent && intent.parts !== undefined) {
209
- if (!Array.isArray(intent.parts)) {
210
- return new Error('Invalid multipart intent: parts must be an array if provided')
211
- }
212
- }
213
- }
214
-
215
- return null
216
- }