@chidchanun/bcp 0.1.26 → 0.1.28
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 +151 -523
- package/docs/README.md +69 -82
- package/docs/production-hardening.md +152 -0
- package/docs/releases/0.1.27.md +350 -0
- package/docs/releases/0.1.28.md +134 -0
- package/docs/s3-storage.md +212 -55
- package/docs/storage-ecosystem.md +434 -0
- package/package.json +2 -1
- package/packages/client/src/server.ts +42 -2
- package/packages/server/src/hardening-proxy.ts +393 -0
- package/packages/server/src/production-hardening.ts +374 -0
- package/packages/server/src/standalone-production-runtime-v7.ts +299 -0
- package/packages/server/src/standalone-production-server.ts +1 -1
- package/packages/server/src/storage-ecosystem.ts +1326 -0
- package/packages/server/src/storage-s3-ecosystem.ts +947 -0
package/docs/s3-storage.md
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
# S3-Compatible Storage
|
|
2
2
|
|
|
3
|
-
BCP Framework `0.1.26`
|
|
3
|
+
BCP Framework `0.1.26` introduced an S3-compatible streaming `StorageAdapter`. BCP `0.1.27` extends the same `createS3Storage()` API with object listing, native copy/move, user metadata, bulk deletion and presigned read/write URLs.
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
> `0.1.27` behavior is an unreleased development target until RC validation and publication complete.
|
|
6
|
+
|
|
7
|
+
The adapter is designed for AWS S3 and services that implement the S3 API, including common S3-compatible providers such as Cloudflare R2 and MinIO. Provider-specific behavior can differ, so production deployments should verify their chosen service with representative object operations.
|
|
6
8
|
|
|
7
9
|
## Create an adapter
|
|
8
10
|
|
|
@@ -54,7 +56,7 @@ const storage =
|
|
|
54
56
|
|
|
55
57
|
## Prefix objects
|
|
56
58
|
|
|
57
|
-
A deployment can reserve a logical prefix without changing application
|
|
59
|
+
A deployment can reserve a logical provider prefix without changing application-facing keys:
|
|
58
60
|
|
|
59
61
|
```ts
|
|
60
62
|
const storage =
|
|
@@ -73,24 +75,22 @@ await storage.put(
|
|
|
73
75
|
);
|
|
74
76
|
```
|
|
75
77
|
|
|
76
|
-
|
|
78
|
+
Application key:
|
|
77
79
|
|
|
78
80
|
```text
|
|
79
81
|
avatars/user-101.webp
|
|
80
82
|
```
|
|
81
83
|
|
|
82
|
-
|
|
84
|
+
Provider key:
|
|
83
85
|
|
|
84
86
|
```text
|
|
85
87
|
production/avatars/user-101.webp
|
|
86
88
|
```
|
|
87
89
|
|
|
88
|
-
|
|
90
|
+
All `0.1.27` list/copy/move/delete/signing operations preserve this logical/provider-key boundary.
|
|
89
91
|
|
|
90
92
|
## Standard operations
|
|
91
93
|
|
|
92
|
-
The S3 adapter implements the same `StorageAdapter` surface as local storage:
|
|
93
|
-
|
|
94
94
|
```ts
|
|
95
95
|
await storage.put(
|
|
96
96
|
"documents/report.pdf",
|
|
@@ -124,7 +124,7 @@ Buffered `put()` uses a conditional S3 write when `overwrite` is not enabled. Ex
|
|
|
124
124
|
|
|
125
125
|
## Streaming upload
|
|
126
126
|
|
|
127
|
-
`createS3Storage()` supports `putStream()` and works with
|
|
127
|
+
`createS3Storage()` supports `putStream()` and works with `putStorageStream()`:
|
|
128
128
|
|
|
129
129
|
```ts
|
|
130
130
|
import {
|
|
@@ -144,21 +144,10 @@ await putStorageStream(
|
|
|
144
144
|
);
|
|
145
145
|
```
|
|
146
146
|
|
|
147
|
-
BCP uses
|
|
148
|
-
|
|
149
|
-
If a stream exceeds `maxBytes`, the upload is aborted and `StorageError` uses:
|
|
150
|
-
|
|
151
|
-
```text
|
|
152
|
-
code: STREAM_TOO_LARGE
|
|
153
|
-
status: 413
|
|
154
|
-
```
|
|
155
|
-
|
|
156
|
-
Multipart uploads use automatic cleanup on failure (`leavePartsOnError: false`).
|
|
147
|
+
BCP uses AWS SDK multipart upload for streams whose total size may not be known when upload begins. Multipart cleanup remains enabled on failure.
|
|
157
148
|
|
|
158
149
|
## Multipart tuning
|
|
159
150
|
|
|
160
|
-
The adapter accepts optional multipart tuning:
|
|
161
|
-
|
|
162
151
|
```ts
|
|
163
152
|
const storage =
|
|
164
153
|
createS3Storage({
|
|
@@ -175,12 +164,10 @@ const storage =
|
|
|
175
164
|
});
|
|
176
165
|
```
|
|
177
166
|
|
|
178
|
-
`partSize` must be at least 5 MiB.
|
|
167
|
+
`partSize` must be at least 5 MiB.
|
|
179
168
|
|
|
180
169
|
## Ranged reads
|
|
181
170
|
|
|
182
|
-
The adapter preserves BCP's inclusive `start` / `end` range contract:
|
|
183
|
-
|
|
184
171
|
```ts
|
|
185
172
|
const bytes =
|
|
186
173
|
await storage.read(
|
|
@@ -194,60 +181,221 @@ const bytes =
|
|
|
194
181
|
|
|
195
182
|
`readStream()` also supports ranges and is used automatically by `createStorageResponse()`.
|
|
196
183
|
|
|
184
|
+
## Listing
|
|
185
|
+
|
|
186
|
+
BCP `0.1.27` enables native S3 `ListObjectsV2` through the generic helper:
|
|
187
|
+
|
|
188
|
+
```ts
|
|
189
|
+
import {
|
|
190
|
+
listStorageObjects,
|
|
191
|
+
} from "bcp/server";
|
|
192
|
+
|
|
193
|
+
const page =
|
|
194
|
+
await listStorageObjects(
|
|
195
|
+
storage,
|
|
196
|
+
{
|
|
197
|
+
prefix:
|
|
198
|
+
"documents/",
|
|
199
|
+
limit:
|
|
200
|
+
100,
|
|
201
|
+
}
|
|
202
|
+
);
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
When S3 returns another page, `page.cursor` contains its continuation token. Treat this cursor as opaque and pass it back unchanged.
|
|
206
|
+
|
|
207
|
+
S3 list responses do not include object content type, so listed `StorageObjectMetadata` uses `application/octet-stream` for `contentType`. Use `stat()` when exact object headers are required.
|
|
208
|
+
|
|
209
|
+
## Native copy and move
|
|
210
|
+
|
|
197
211
|
```ts
|
|
198
|
-
|
|
199
|
-
|
|
212
|
+
import {
|
|
213
|
+
copyStorageObject,
|
|
214
|
+
moveStorageObject,
|
|
215
|
+
} from "bcp/server";
|
|
216
|
+
|
|
217
|
+
await copyStorageObject(
|
|
200
218
|
storage,
|
|
201
|
-
"
|
|
219
|
+
"incoming/report.pdf",
|
|
220
|
+
"archive/report.pdf"
|
|
221
|
+
);
|
|
222
|
+
|
|
223
|
+
await moveStorageObject(
|
|
224
|
+
storage,
|
|
225
|
+
"incoming/avatar.webp",
|
|
226
|
+
"users/42/avatar.webp"
|
|
202
227
|
);
|
|
203
228
|
```
|
|
204
229
|
|
|
205
|
-
|
|
230
|
+
The S3 adapter uses native `CopyObject`. User metadata and BCP checksum metadata are preserved by normal copies.
|
|
231
|
+
|
|
232
|
+
Move is copy followed by source deletion. It is not a distributed transaction; applications needing business-level atomicity should coordinate storage state with their database.
|
|
233
|
+
|
|
234
|
+
## User metadata
|
|
235
|
+
|
|
236
|
+
```ts
|
|
237
|
+
await storage.put(
|
|
238
|
+
"documents/report.pdf",
|
|
239
|
+
bytes,
|
|
240
|
+
{
|
|
241
|
+
contentType:
|
|
242
|
+
"application/pdf",
|
|
243
|
+
metadata: {
|
|
244
|
+
owner:
|
|
245
|
+
"user-42",
|
|
246
|
+
category:
|
|
247
|
+
"report",
|
|
248
|
+
},
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Read or replace metadata:
|
|
254
|
+
|
|
255
|
+
```ts
|
|
256
|
+
import {
|
|
257
|
+
getStorageMetadata,
|
|
258
|
+
setStorageMetadata,
|
|
259
|
+
} from "bcp/server";
|
|
260
|
+
|
|
261
|
+
const metadata =
|
|
262
|
+
await getStorageMetadata(
|
|
263
|
+
storage,
|
|
264
|
+
"documents/report.pdf"
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
await setStorageMetadata(
|
|
268
|
+
storage,
|
|
269
|
+
"documents/report.pdf",
|
|
270
|
+
{
|
|
271
|
+
owner:
|
|
272
|
+
"user-42",
|
|
273
|
+
state:
|
|
274
|
+
"approved",
|
|
275
|
+
}
|
|
276
|
+
);
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
BCP maps application metadata to S3 user metadata and reserves `bcp_sha256` for framework checksum data.
|
|
280
|
+
|
|
281
|
+
Replacing S3 metadata uses provider copy-to-self semantics with `MetadataDirective: REPLACE` while retaining BCP checksum metadata.
|
|
282
|
+
|
|
283
|
+
## Bulk deletion
|
|
284
|
+
|
|
285
|
+
```ts
|
|
286
|
+
import {
|
|
287
|
+
deleteStorageObjects,
|
|
288
|
+
} from "bcp/server";
|
|
289
|
+
|
|
290
|
+
const result =
|
|
291
|
+
await deleteStorageObjects(
|
|
292
|
+
storage,
|
|
293
|
+
keys
|
|
294
|
+
);
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
BCP batches native S3 multi-delete requests at up to 1000 keys per request.
|
|
298
|
+
|
|
299
|
+
S3 delete semantics are idempotent: deleting a missing key is normally reported as successful. Therefore S3 results commonly place such keys in `deleted` rather than `missing`.
|
|
300
|
+
|
|
301
|
+
## Presigned read URLs
|
|
302
|
+
|
|
303
|
+
```ts
|
|
304
|
+
import {
|
|
305
|
+
createStorageSignedReadUrl,
|
|
306
|
+
} from "bcp/server";
|
|
307
|
+
|
|
308
|
+
const url =
|
|
309
|
+
await createStorageSignedReadUrl(
|
|
310
|
+
storage,
|
|
311
|
+
"videos/demo.mp4",
|
|
312
|
+
{
|
|
313
|
+
expiresIn:
|
|
314
|
+
300,
|
|
315
|
+
}
|
|
316
|
+
);
|
|
317
|
+
```
|
|
318
|
+
|
|
319
|
+
The default expiry is 15 minutes. The maximum accepted expiry is seven days, matching the SigV4 presigning boundary.
|
|
320
|
+
|
|
321
|
+
## Presigned write URLs
|
|
322
|
+
|
|
323
|
+
```ts
|
|
324
|
+
import {
|
|
325
|
+
createStorageSignedWriteUrl,
|
|
326
|
+
} from "bcp/server";
|
|
327
|
+
|
|
328
|
+
const url =
|
|
329
|
+
await createStorageSignedWriteUrl(
|
|
330
|
+
storage,
|
|
331
|
+
"uploads/video.mp4",
|
|
332
|
+
{
|
|
333
|
+
expiresIn:
|
|
334
|
+
300,
|
|
335
|
+
contentType:
|
|
336
|
+
"video/mp4",
|
|
337
|
+
metadata: {
|
|
338
|
+
owner:
|
|
339
|
+
"user-42",
|
|
340
|
+
},
|
|
341
|
+
}
|
|
342
|
+
);
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
The browser or another client can then upload directly to S3-compatible storage instead of proxying the file bytes through BCP.
|
|
346
|
+
|
|
347
|
+
When content type or metadata are included in the signed operation, the direct uploader must send the headers required by the generated signature/provider.
|
|
348
|
+
|
|
349
|
+
Never issue a signed URL before application authorization. Signed URLs should be treated as temporary credentials.
|
|
206
350
|
|
|
207
351
|
## Capabilities
|
|
208
352
|
|
|
353
|
+
Use the original capability helper for compatibility:
|
|
354
|
+
|
|
209
355
|
```ts
|
|
210
356
|
getStorageCapabilities(storage)
|
|
211
357
|
```
|
|
212
358
|
|
|
213
|
-
|
|
359
|
+
For the `0.1.27` S3 wrapper it reports:
|
|
214
360
|
|
|
215
361
|
```text
|
|
216
362
|
streamingRead: true
|
|
217
363
|
streamingWrite: true
|
|
218
364
|
ranges: true
|
|
219
|
-
signedUrls:
|
|
220
|
-
listing:
|
|
365
|
+
signedUrls: true
|
|
366
|
+
listing: true
|
|
221
367
|
```
|
|
222
368
|
|
|
223
|
-
|
|
369
|
+
For the complete ecosystem surface use:
|
|
224
370
|
|
|
225
|
-
|
|
371
|
+
```ts
|
|
372
|
+
getStorageEcosystemCapabilities(storage)
|
|
373
|
+
```
|
|
226
374
|
|
|
227
|
-
|
|
375
|
+
which additionally reports:
|
|
228
376
|
|
|
229
|
-
|
|
377
|
+
```text
|
|
378
|
+
signedReadUrls: true
|
|
379
|
+
signedWriteUrls: true
|
|
380
|
+
copy: true
|
|
381
|
+
move: true
|
|
382
|
+
metadata: true
|
|
383
|
+
bulkDelete: true
|
|
384
|
+
```
|
|
230
385
|
|
|
231
|
-
|
|
386
|
+
## Overwrite semantics
|
|
232
387
|
|
|
233
|
-
|
|
388
|
+
Buffered `put()` retains the `0.1.26` conditional create-only behavior when `overwrite` is false.
|
|
234
389
|
|
|
235
|
-
S3
|
|
390
|
+
Streaming multipart writes still use a preflight existence check because portable S3 multipart APIs do not expose the same atomic create-only condition across every compatible provider.
|
|
236
391
|
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
size,
|
|
241
|
-
contentType,
|
|
242
|
-
lastModified,
|
|
243
|
-
etag,
|
|
244
|
-
checksumSha256?,
|
|
245
|
-
}
|
|
246
|
-
```
|
|
392
|
+
Copy/move destinations also reject existing objects by default unless `overwrite: true` is supplied.
|
|
393
|
+
|
|
394
|
+
## Metadata and ETags
|
|
247
395
|
|
|
248
|
-
|
|
396
|
+
Do not assume an S3 ETag is a plain MD5 checksum. Multipart uploads and provider-specific implementations can use different ETag formats.
|
|
249
397
|
|
|
250
|
-
|
|
398
|
+
Buffered BCP writes persist a SHA-256 value in reserved S3 user metadata. Multipart streams may not expose a portable whole-object SHA-256 through the generic contract.
|
|
251
399
|
|
|
252
400
|
## Client lifecycle
|
|
253
401
|
|
|
@@ -263,14 +411,23 @@ When an existing `S3Client` is injected through `client`, BCP does not destroy t
|
|
|
263
411
|
|
|
264
412
|
## Security
|
|
265
413
|
|
|
266
|
-
Storage keys are not authorization. Authenticate and authorize
|
|
414
|
+
Storage keys are not authorization. Authenticate and authorize before exposing object operations.
|
|
267
415
|
|
|
268
|
-
Recommended production practices
|
|
416
|
+
Recommended production practices:
|
|
269
417
|
|
|
270
418
|
- keep buckets private by default,
|
|
271
419
|
- use scoped credentials / IAM policies,
|
|
272
420
|
- do not expose access keys to browser bundles,
|
|
421
|
+
- use short presigned URL expirations,
|
|
422
|
+
- avoid logging full signed query strings,
|
|
273
423
|
- validate uploaded content independently of MIME metadata,
|
|
274
|
-
- configure
|
|
275
|
-
- keep application upload limits below infrastructure
|
|
276
|
-
-
|
|
424
|
+
- configure encryption/retention policies when required,
|
|
425
|
+
- keep application upload limits below proxy/infrastructure limits,
|
|
426
|
+
- configure CORS only for the browser origins and methods that need direct object access.
|
|
427
|
+
|
|
428
|
+
## Related documentation
|
|
429
|
+
|
|
430
|
+
- [Storage and File Delivery](storage.md)
|
|
431
|
+
- [Storage Ecosystem](storage-ecosystem.md)
|
|
432
|
+
- [File Upload](file-upload.md)
|
|
433
|
+
- [BCP Framework 0.1.27](releases/0.1.27.md)
|