@chidchanun/bcp 0.1.25 → 0.1.26
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +169 -263
- package/docs/README.md +148 -210
- package/docs/file-upload.md +194 -32
- package/docs/releases/0.1.26.md +281 -0
- package/docs/s3-storage.md +276 -0
- package/docs/storage.md +278 -162
- package/package.json +5 -2
- package/packages/client/src/database.mjs +236 -0
- package/packages/client/src/server.ts +22 -0
- package/packages/server/src/file-delivery.ts +8 -24
- package/packages/server/src/storage-s3.ts +1159 -0
- package/packages/server/src/storage.ts +740 -54
- package/packages/server/src/upload-stream.ts +846 -0
package/docs/storage.md
CHANGED
|
@@ -1,80 +1,87 @@
|
|
|
1
1
|
# Storage and File Delivery
|
|
2
2
|
|
|
3
|
-
BCP Framework `0.1.25`
|
|
3
|
+
BCP Framework `0.1.25` introduced the server-only `StorageAdapter` abstraction and production file-delivery helpers through `bcp/server`.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
BCP Framework `0.1.26` completes the next storage milestone with:
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
- additive streaming reads/writes,
|
|
8
|
+
- capability discovery,
|
|
9
|
+
- local streaming I/O,
|
|
10
|
+
- S3-compatible storage,
|
|
11
|
+
- production multipart-to-storage streaming,
|
|
12
|
+
- streamed full/ranged file delivery.
|
|
13
|
+
|
|
14
|
+
Existing adapters written for `0.1.25` remain valid because `putStream()` and `readStream()` are optional.
|
|
15
|
+
|
|
16
|
+
## Local storage
|
|
8
17
|
|
|
9
18
|
```ts
|
|
10
19
|
import {
|
|
11
20
|
createLocalStorage,
|
|
12
21
|
} from "bcp/server";
|
|
13
22
|
|
|
14
|
-
export const
|
|
23
|
+
export const storage =
|
|
15
24
|
createLocalStorage({
|
|
16
25
|
directory:
|
|
17
26
|
"./uploads",
|
|
18
27
|
});
|
|
19
28
|
```
|
|
20
29
|
|
|
21
|
-
Storage keys are relative
|
|
30
|
+
Storage keys are logical relative paths:
|
|
22
31
|
|
|
23
32
|
```text
|
|
24
33
|
avatars/user-101.webp
|
|
25
34
|
documents/2026/report.pdf
|
|
26
35
|
```
|
|
27
36
|
|
|
28
|
-
Absolute paths,
|
|
37
|
+
Absolute paths, traversal segments and BCP's reserved metadata directory are rejected.
|
|
29
38
|
|
|
30
|
-
##
|
|
39
|
+
## S3-compatible storage
|
|
31
40
|
|
|
32
41
|
```ts
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
"Hello BCP",
|
|
37
|
-
{
|
|
38
|
-
contentType:
|
|
39
|
-
"text/plain",
|
|
40
|
-
}
|
|
41
|
-
);
|
|
42
|
+
import {
|
|
43
|
+
createS3Storage,
|
|
44
|
+
} from "bcp/server";
|
|
42
45
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
46
|
+
export const storage =
|
|
47
|
+
createS3Storage({
|
|
48
|
+
bucket:
|
|
49
|
+
process.env.S3_BUCKET!,
|
|
50
|
+
region:
|
|
51
|
+
process.env.S3_REGION!,
|
|
52
|
+
endpoint:
|
|
53
|
+
process.env.S3_ENDPOINT,
|
|
54
|
+
accessKeyId:
|
|
55
|
+
process.env.S3_ACCESS_KEY_ID,
|
|
56
|
+
secretAccessKey:
|
|
57
|
+
process.env.S3_SECRET_ACCESS_KEY,
|
|
58
|
+
});
|
|
47
59
|
```
|
|
48
60
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
The local adapter stores metadata separately from the object bytes so content type and SHA-256 identity survive process restarts.
|
|
61
|
+
The same application-facing `StorageAdapter` surface works with local or S3-compatible backends.
|
|
52
62
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
```ts
|
|
56
|
-
{
|
|
57
|
-
key,
|
|
58
|
-
size,
|
|
59
|
-
contentType,
|
|
60
|
-
lastModified,
|
|
61
|
-
etag,
|
|
62
|
-
checksumSha256,
|
|
63
|
-
}
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Objects created through the local adapter use a strong SHA-256 ETag.
|
|
63
|
+
Read more: [S3-Compatible Storage](s3-storage.md).
|
|
67
64
|
|
|
68
65
|
## Storage adapter contract
|
|
69
66
|
|
|
70
67
|
```ts
|
|
71
68
|
interface StorageAdapter {
|
|
69
|
+
capabilities?: Partial<
|
|
70
|
+
StorageAdapterCapabilities
|
|
71
|
+
>;
|
|
72
|
+
|
|
72
73
|
put(
|
|
73
74
|
key: string,
|
|
74
75
|
value: StorageWriteValue,
|
|
75
76
|
options?: StoragePutOptions
|
|
76
77
|
): Promise<StorageObjectMetadata>;
|
|
77
78
|
|
|
79
|
+
putStream?(
|
|
80
|
+
key: string,
|
|
81
|
+
stream: ReadableStream<Uint8Array>,
|
|
82
|
+
options?: StoragePutStreamOptions
|
|
83
|
+
): Promise<StorageObjectMetadata>;
|
|
84
|
+
|
|
78
85
|
stat(
|
|
79
86
|
key: string
|
|
80
87
|
): Promise<StorageObjectMetadata | null>;
|
|
@@ -84,6 +91,11 @@ interface StorageAdapter {
|
|
|
84
91
|
options?: StorageReadOptions
|
|
85
92
|
): Promise<Uint8Array>;
|
|
86
93
|
|
|
94
|
+
readStream?(
|
|
95
|
+
key: string,
|
|
96
|
+
options?: StorageReadOptions
|
|
97
|
+
): Promise<StorageReadableStream>;
|
|
98
|
+
|
|
87
99
|
exists(
|
|
88
100
|
key: string
|
|
89
101
|
): Promise<boolean>;
|
|
@@ -94,173 +106,274 @@ interface StorageAdapter {
|
|
|
94
106
|
}
|
|
95
107
|
```
|
|
96
108
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
## Upload directly into storage
|
|
109
|
+
The original buffered methods remain required. Streaming methods are additive optional capabilities.
|
|
100
110
|
|
|
101
|
-
|
|
111
|
+
## Capabilities
|
|
102
112
|
|
|
103
113
|
```ts
|
|
104
114
|
import {
|
|
105
|
-
|
|
106
|
-
parseMultipartFormData,
|
|
107
|
-
requireUploadedFile,
|
|
108
|
-
storeUploadedFile,
|
|
115
|
+
getStorageCapabilities,
|
|
109
116
|
} from "bcp/server";
|
|
110
117
|
|
|
111
|
-
const
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
"./uploads",
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
export async function POST(
|
|
118
|
-
request: Request
|
|
119
|
-
) {
|
|
120
|
-
const formData =
|
|
121
|
-
await parseMultipartFormData(
|
|
122
|
-
request,
|
|
123
|
-
{
|
|
124
|
-
maxBytes:
|
|
125
|
-
8 * 1024 * 1024,
|
|
126
|
-
}
|
|
127
|
-
);
|
|
128
|
-
|
|
129
|
-
const file =
|
|
130
|
-
requireUploadedFile(
|
|
131
|
-
formData,
|
|
132
|
-
"file",
|
|
133
|
-
{
|
|
134
|
-
maxBytes:
|
|
135
|
-
5 * 1024 * 1024,
|
|
136
|
-
allowedTypes: [
|
|
137
|
-
"image/png",
|
|
138
|
-
"image/jpeg",
|
|
139
|
-
"image/webp",
|
|
140
|
-
],
|
|
141
|
-
}
|
|
142
|
-
);
|
|
143
|
-
|
|
144
|
-
const stored =
|
|
145
|
-
await storeUploadedFile(
|
|
146
|
-
file,
|
|
147
|
-
{
|
|
148
|
-
storage:
|
|
149
|
-
uploads,
|
|
150
|
-
key:
|
|
151
|
-
`avatars/${crypto.randomUUID()}.webp`,
|
|
152
|
-
}
|
|
153
|
-
);
|
|
154
|
-
|
|
155
|
-
return Response.json(
|
|
156
|
-
stored
|
|
118
|
+
const capabilities =
|
|
119
|
+
getStorageCapabilities(
|
|
120
|
+
storage
|
|
157
121
|
);
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
The capability model currently reports:
|
|
125
|
+
|
|
126
|
+
```ts
|
|
127
|
+
{
|
|
128
|
+
streamingRead,
|
|
129
|
+
streamingWrite,
|
|
130
|
+
ranges,
|
|
131
|
+
signedUrls,
|
|
132
|
+
listing,
|
|
158
133
|
}
|
|
159
134
|
```
|
|
160
135
|
|
|
161
|
-
|
|
136
|
+
`createLocalStorage()` and `createS3Storage()` in `0.1.26` report:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
{
|
|
140
|
+
streamingRead: true,
|
|
141
|
+
streamingWrite: true,
|
|
142
|
+
ranges: true,
|
|
143
|
+
signedUrls: false,
|
|
144
|
+
listing: false,
|
|
145
|
+
}
|
|
146
|
+
```
|
|
162
147
|
|
|
163
|
-
|
|
148
|
+
Signed URLs and listing are deferred to a later milestone.
|
|
164
149
|
|
|
165
|
-
|
|
150
|
+
## Buffered writes
|
|
151
|
+
|
|
152
|
+
```ts
|
|
153
|
+
const stored =
|
|
154
|
+
await storage.put(
|
|
155
|
+
"notes/hello.txt",
|
|
156
|
+
"Hello BCP",
|
|
157
|
+
{
|
|
158
|
+
contentType:
|
|
159
|
+
"text/plain",
|
|
160
|
+
}
|
|
161
|
+
);
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`put()` does not overwrite an existing object unless `overwrite: true` is supplied.
|
|
165
|
+
|
|
166
|
+
Local storage persists content type and SHA-256 identity in sidecar metadata. Buffered S3 writes persist SHA-256 in S3 user metadata.
|
|
167
|
+
|
|
168
|
+
## Streaming writes
|
|
169
|
+
|
|
170
|
+
Use the generic helper so application code works with native or legacy adapters:
|
|
166
171
|
|
|
167
172
|
```ts
|
|
168
173
|
import {
|
|
169
|
-
|
|
170
|
-
createStorageResponse,
|
|
174
|
+
putStorageStream,
|
|
171
175
|
} from "bcp/server";
|
|
172
176
|
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
177
|
+
await putStorageStream(
|
|
178
|
+
storage,
|
|
179
|
+
"imports/data.bin",
|
|
180
|
+
request.body!,
|
|
181
|
+
{
|
|
182
|
+
contentType:
|
|
183
|
+
"application/octet-stream",
|
|
184
|
+
maxBytes:
|
|
185
|
+
100 * 1024 * 1024,
|
|
186
|
+
}
|
|
187
|
+
);
|
|
188
|
+
```
|
|
178
189
|
|
|
179
|
-
|
|
180
|
-
|
|
190
|
+
When an adapter provides `putStream()`, BCP uses it. Legacy adapters fall back to collecting the stream and calling `put()`.
|
|
191
|
+
|
|
192
|
+
The local adapter writes chunks directly to disk and calculates SHA-256 while streaming.
|
|
193
|
+
|
|
194
|
+
The S3 adapter uses the AWS SDK multipart upload helper so streams whose final size is not known at the start can still be uploaded without buffering the complete object.
|
|
195
|
+
|
|
196
|
+
### Size limits and abort
|
|
197
|
+
|
|
198
|
+
```ts
|
|
199
|
+
await putStorageStream(
|
|
200
|
+
storage,
|
|
201
|
+
key,
|
|
202
|
+
source,
|
|
181
203
|
{
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
};
|
|
204
|
+
maxBytes:
|
|
205
|
+
50 * 1024 * 1024,
|
|
206
|
+
signal:
|
|
207
|
+
abortController.signal,
|
|
187
208
|
}
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
209
|
+
);
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
If the stream exceeds `maxBytes`, BCP throws:
|
|
213
|
+
|
|
214
|
+
```text
|
|
215
|
+
StorageError
|
|
216
|
+
code: STREAM_TOO_LARGE
|
|
217
|
+
status: 413
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Native adapters clean up/abort incomplete writes where supported.
|
|
221
|
+
|
|
222
|
+
## Streaming reads
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
import {
|
|
226
|
+
readStorageStream,
|
|
227
|
+
} from "bcp/server";
|
|
228
|
+
|
|
229
|
+
const stream =
|
|
230
|
+
await readStorageStream(
|
|
231
|
+
storage,
|
|
232
|
+
"videos/demo.mp4"
|
|
233
|
+
);
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
Inclusive byte ranges are supported:
|
|
237
|
+
|
|
238
|
+
```ts
|
|
239
|
+
const stream =
|
|
240
|
+
await readStorageStream(
|
|
241
|
+
storage,
|
|
242
|
+
"videos/demo.mp4",
|
|
193
243
|
{
|
|
194
|
-
|
|
195
|
-
|
|
244
|
+
start: 0,
|
|
245
|
+
end: 1023,
|
|
196
246
|
}
|
|
197
247
|
);
|
|
198
|
-
}
|
|
199
248
|
```
|
|
200
249
|
|
|
201
|
-
|
|
250
|
+
Legacy adapters fall back to `read()` and expose the result as a stream.
|
|
202
251
|
|
|
203
|
-
|
|
204
|
-
- `Accept-Ranges: bytes`.
|
|
205
|
-
- single byte ranges with `206 Partial Content`.
|
|
206
|
-
- open-ended ranges such as `bytes=500-`.
|
|
207
|
-
- suffix ranges such as `bytes=-500`.
|
|
208
|
-
- `416 Range Not Satisfiable` for invalid/unsupported ranges.
|
|
209
|
-
- `ETag` and `If-None-Match` validation.
|
|
210
|
-
- `Last-Modified` and `If-Modified-Since` validation.
|
|
211
|
-
- `If-Range` handling.
|
|
212
|
-
- configurable `Cache-Control`.
|
|
213
|
-
- safe `Content-Disposition` filenames.
|
|
252
|
+
## Multipart uploads
|
|
214
253
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
## Downloads
|
|
254
|
+
Small forms can continue using the buffered `FormData` API:
|
|
218
255
|
|
|
219
256
|
```ts
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
257
|
+
const formData =
|
|
258
|
+
await parseMultipartFormData(
|
|
259
|
+
request
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const file =
|
|
263
|
+
requireUploadedFile(
|
|
264
|
+
formData,
|
|
265
|
+
"file"
|
|
266
|
+
);
|
|
267
|
+
|
|
268
|
+
await storeUploadedFile(
|
|
269
|
+
file,
|
|
224
270
|
{
|
|
225
|
-
|
|
226
|
-
"attachment",
|
|
227
|
-
downloadName:
|
|
228
|
-
"monthly-report.pdf",
|
|
271
|
+
storage,
|
|
229
272
|
}
|
|
230
273
|
);
|
|
231
274
|
```
|
|
232
275
|
|
|
233
|
-
|
|
276
|
+
For large production uploads, `0.1.26` adds direct multipart-to-storage streaming:
|
|
234
277
|
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
278
|
+
```ts
|
|
279
|
+
import {
|
|
280
|
+
storeMultipartFile,
|
|
281
|
+
} from "bcp/server";
|
|
238
282
|
|
|
239
|
-
|
|
240
|
-
|
|
283
|
+
const stored =
|
|
284
|
+
await storeMultipartFile(
|
|
285
|
+
request,
|
|
286
|
+
{
|
|
287
|
+
storage,
|
|
288
|
+
fieldName:
|
|
289
|
+
"file",
|
|
290
|
+
key:
|
|
291
|
+
"documents/report.pdf",
|
|
292
|
+
maxBytes:
|
|
293
|
+
100 * 1024 * 1024,
|
|
294
|
+
constraints: {
|
|
295
|
+
maxBytes:
|
|
296
|
+
80 * 1024 * 1024,
|
|
297
|
+
allowedTypes: [
|
|
298
|
+
"application/pdf",
|
|
299
|
+
],
|
|
300
|
+
allowedExtensions: [
|
|
301
|
+
".pdf",
|
|
302
|
+
],
|
|
303
|
+
},
|
|
304
|
+
}
|
|
305
|
+
);
|
|
241
306
|
```
|
|
242
307
|
|
|
243
|
-
|
|
308
|
+
The multipart parser and storage write operate concurrently, so the complete file is not converted to a `File` / `ArrayBuffer` first.
|
|
309
|
+
|
|
310
|
+
Read more: [File Upload](file-upload.md).
|
|
311
|
+
|
|
312
|
+
## File delivery
|
|
244
313
|
|
|
245
314
|
```ts
|
|
246
|
-
{
|
|
247
|
-
|
|
248
|
-
|
|
315
|
+
import {
|
|
316
|
+
createStorageResponse,
|
|
317
|
+
} from "bcp/server";
|
|
318
|
+
|
|
319
|
+
export function GET(
|
|
320
|
+
request: Request
|
|
321
|
+
) {
|
|
322
|
+
return createStorageResponse(
|
|
323
|
+
request,
|
|
324
|
+
storage,
|
|
325
|
+
"documents/report.pdf",
|
|
326
|
+
{
|
|
327
|
+
disposition:
|
|
328
|
+
"attachment",
|
|
329
|
+
downloadName:
|
|
330
|
+
"report.pdf",
|
|
331
|
+
}
|
|
332
|
+
);
|
|
249
333
|
}
|
|
250
334
|
```
|
|
251
335
|
|
|
252
|
-
|
|
336
|
+
`createStorageResponse()` uses `readStorageStream()` and supports:
|
|
253
337
|
|
|
254
|
-
|
|
338
|
+
- `GET` and `HEAD`,
|
|
339
|
+
- `ETag` / `If-None-Match`,
|
|
340
|
+
- `Last-Modified` / `If-Modified-Since`,
|
|
341
|
+
- `If-Range`,
|
|
342
|
+
- single byte ranges with `206 Partial Content`,
|
|
343
|
+
- `304 Not Modified`,
|
|
344
|
+
- `416 Range Not Satisfiable`,
|
|
345
|
+
- safe `Content-Disposition` filenames,
|
|
346
|
+
- configurable cache control.
|
|
347
|
+
|
|
348
|
+
The default cache policy is:
|
|
349
|
+
|
|
350
|
+
```text
|
|
351
|
+
Cache-Control: private, max-age=0, must-revalidate
|
|
352
|
+
```
|
|
255
353
|
|
|
256
|
-
|
|
354
|
+
Public immutable caching must be enabled explicitly.
|
|
355
|
+
|
|
356
|
+
Multiple byte ranges are intentionally unsupported in `0.1.26`.
|
|
357
|
+
|
|
358
|
+
## Object metadata
|
|
257
359
|
|
|
258
360
|
```ts
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
361
|
+
interface StorageObjectMetadata {
|
|
362
|
+
key: string;
|
|
363
|
+
size: number;
|
|
364
|
+
contentType: string;
|
|
365
|
+
lastModified: Date;
|
|
366
|
+
etag: string;
|
|
367
|
+
checksumSha256?: string;
|
|
368
|
+
}
|
|
262
369
|
```
|
|
263
370
|
|
|
371
|
+
`checksumSha256` is optional because not every cloud provider exposes a portable whole-object SHA-256 value for multipart objects.
|
|
372
|
+
|
|
373
|
+
Do not assume an S3 ETag is an MD5 hash.
|
|
374
|
+
|
|
375
|
+
## Storage errors
|
|
376
|
+
|
|
264
377
|
Current codes:
|
|
265
378
|
|
|
266
379
|
```text
|
|
@@ -268,18 +381,21 @@ INVALID_KEY
|
|
|
268
381
|
OBJECT_NOT_FOUND
|
|
269
382
|
OBJECT_EXISTS
|
|
270
383
|
RANGE_NOT_SATISFIABLE
|
|
384
|
+
STREAM_TOO_LARGE
|
|
271
385
|
```
|
|
272
386
|
|
|
273
|
-
`createStorageResponse()` converts ordinary missing objects and invalid HTTP ranges into `404` / `416` responses directly.
|
|
274
|
-
|
|
275
387
|
## Security boundary
|
|
276
388
|
|
|
277
|
-
Storage keys are not authorization.
|
|
389
|
+
Storage keys are not authorization. Authenticate and authorize requests before allowing access to user-selected keys.
|
|
278
390
|
|
|
279
|
-
|
|
391
|
+
MIME type and extension checks are metadata validation, not file-signature verification.
|
|
280
392
|
|
|
281
|
-
|
|
393
|
+
Streaming limits do not replace the framework/proxy request body limit. Configure `server.bodyLimit` / `BCP_BODY_LIMIT` large enough for intended uploads while keeping an explicit outer request boundary.
|
|
282
394
|
|
|
283
|
-
|
|
395
|
+
For S3/object storage:
|
|
284
396
|
|
|
285
|
-
|
|
397
|
+
- keep credentials server-only,
|
|
398
|
+
- prefer private buckets,
|
|
399
|
+
- scope IAM/provider permissions,
|
|
400
|
+
- use provider-side encryption/retention policies where required,
|
|
401
|
+
- validate provider CORS only when direct browser access is intentionally enabled.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chidchanun/bcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.26",
|
|
4
4
|
"description": "BCP Framework - a React full-stack framework with file-based routing, SSR, APIs, middleware, islands, caching and standalone production builds.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -55,7 +55,7 @@
|
|
|
55
55
|
"./database": {
|
|
56
56
|
"types": "./packages/client/src/database.ts",
|
|
57
57
|
"browser": "./packages/client/src/server-only.browser.mjs",
|
|
58
|
-
"default": "./packages/client/src/database.
|
|
58
|
+
"default": "./packages/client/src/database.mjs"
|
|
59
59
|
},
|
|
60
60
|
"./auth": {
|
|
61
61
|
"types": "./packages/client/src/auth.ts",
|
|
@@ -79,9 +79,12 @@
|
|
|
79
79
|
"./package.json": "./package.json"
|
|
80
80
|
},
|
|
81
81
|
"dependencies": {
|
|
82
|
+
"@aws-sdk/client-s3": "^3.1119.0",
|
|
83
|
+
"@aws-sdk/lib-storage": "^3.1119.0",
|
|
82
84
|
"@babel/core": "^8.0.1",
|
|
83
85
|
"@babel/preset-react": "^8.0.1",
|
|
84
86
|
"@babel/preset-typescript": "^8.0.1",
|
|
87
|
+
"busboy": "^1.6.0",
|
|
85
88
|
"chokidar": "^5.0.0",
|
|
86
89
|
"esbuild": "^0.28.2",
|
|
87
90
|
"react-refresh": "^0.18.0",
|