@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.
@@ -0,0 +1,276 @@
1
+ # S3-Compatible Storage
2
+
3
+ BCP Framework `0.1.26` adds an S3-compatible `StorageAdapter` implementation through `bcp/server`.
4
+
5
+ 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 still differ, so production deployments should verify their chosen provider with representative uploads, ranges and overwrite rules.
6
+
7
+ ## Create an adapter
8
+
9
+ ```ts
10
+ import {
11
+ createS3Storage,
12
+ } from "bcp/server";
13
+
14
+ export const storage =
15
+ createS3Storage({
16
+ bucket:
17
+ process.env.S3_BUCKET!,
18
+ region:
19
+ process.env.S3_REGION!,
20
+ endpoint:
21
+ process.env.S3_ENDPOINT,
22
+ accessKeyId:
23
+ process.env.S3_ACCESS_KEY_ID,
24
+ secretAccessKey:
25
+ process.env.S3_SECRET_ACCESS_KEY,
26
+ });
27
+ ```
28
+
29
+ When `accessKeyId` / `secretAccessKey` are omitted, the underlying AWS SDK client can use its normal server-side credential provider chain.
30
+
31
+ Do not expose S3 credentials through `BCP_PUBLIC_*` environment variables.
32
+
33
+ ## MinIO / path-style endpoints
34
+
35
+ Some S3-compatible endpoints require path-style addressing:
36
+
37
+ ```ts
38
+ const storage =
39
+ createS3Storage({
40
+ bucket:
41
+ "uploads",
42
+ region:
43
+ "us-east-1",
44
+ endpoint:
45
+ "http://127.0.0.1:9000",
46
+ forcePathStyle:
47
+ true,
48
+ accessKeyId:
49
+ process.env.MINIO_ACCESS_KEY,
50
+ secretAccessKey:
51
+ process.env.MINIO_SECRET_KEY,
52
+ });
53
+ ```
54
+
55
+ ## Prefix objects
56
+
57
+ A deployment can reserve a logical prefix without changing application storage keys:
58
+
59
+ ```ts
60
+ const storage =
61
+ createS3Storage({
62
+ bucket:
63
+ "app-data",
64
+ region:
65
+ "auto",
66
+ prefix:
67
+ "production",
68
+ });
69
+
70
+ await storage.put(
71
+ "avatars/user-101.webp",
72
+ bytes
73
+ );
74
+ ```
75
+
76
+ The application-facing key remains:
77
+
78
+ ```text
79
+ avatars/user-101.webp
80
+ ```
81
+
82
+ while the provider object key becomes:
83
+
84
+ ```text
85
+ production/avatars/user-101.webp
86
+ ```
87
+
88
+ Prefixes and object keys reject traversal segments such as `..`.
89
+
90
+ ## Standard operations
91
+
92
+ The S3 adapter implements the same `StorageAdapter` surface as local storage:
93
+
94
+ ```ts
95
+ await storage.put(
96
+ "documents/report.pdf",
97
+ bytes,
98
+ {
99
+ contentType:
100
+ "application/pdf",
101
+ }
102
+ );
103
+
104
+ const metadata =
105
+ await storage.stat(
106
+ "documents/report.pdf"
107
+ );
108
+
109
+ const bytes =
110
+ await storage.read(
111
+ "documents/report.pdf"
112
+ );
113
+
114
+ await storage.exists(
115
+ "documents/report.pdf"
116
+ );
117
+
118
+ await storage.delete(
119
+ "documents/report.pdf"
120
+ );
121
+ ```
122
+
123
+ Buffered `put()` uses a conditional S3 write when `overwrite` is not enabled. Existing objects therefore produce `StorageError` with code `OBJECT_EXISTS` instead of being silently replaced.
124
+
125
+ ## Streaming upload
126
+
127
+ `createS3Storage()` supports `putStream()` and works with the generic `putStorageStream()` helper:
128
+
129
+ ```ts
130
+ import {
131
+ putStorageStream,
132
+ } from "bcp/server";
133
+
134
+ await putStorageStream(
135
+ storage,
136
+ "media/video.mp4",
137
+ request.body!,
138
+ {
139
+ contentType:
140
+ "video/mp4",
141
+ maxBytes:
142
+ 500 * 1024 * 1024,
143
+ }
144
+ );
145
+ ```
146
+
147
+ BCP uses the AWS SDK multipart upload helper for streams whose total size may not be known when the upload begins. This avoids requiring the complete object to be buffered in application memory.
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`).
157
+
158
+ ## Multipart tuning
159
+
160
+ The adapter accepts optional multipart tuning:
161
+
162
+ ```ts
163
+ const storage =
164
+ createS3Storage({
165
+ bucket:
166
+ "uploads",
167
+ region:
168
+ "us-east-1",
169
+ multipart: {
170
+ partSize:
171
+ 8 * 1024 * 1024,
172
+ queueSize:
173
+ 2,
174
+ },
175
+ });
176
+ ```
177
+
178
+ `partSize` must be at least 5 MiB. Higher `queueSize` can improve throughput, but it also increases concurrent network and memory usage.
179
+
180
+ ## Ranged reads
181
+
182
+ The adapter preserves BCP's inclusive `start` / `end` range contract:
183
+
184
+ ```ts
185
+ const bytes =
186
+ await storage.read(
187
+ "media/video.mp4",
188
+ {
189
+ start: 0,
190
+ end: 1023,
191
+ }
192
+ );
193
+ ```
194
+
195
+ `readStream()` also supports ranges and is used automatically by `createStorageResponse()`.
196
+
197
+ ```ts
198
+ return createStorageResponse(
199
+ request,
200
+ storage,
201
+ "media/video.mp4"
202
+ );
203
+ ```
204
+
205
+ This allows the existing file-delivery API to serve local or S3-backed objects with the same `GET`, `HEAD`, ETag, Last-Modified and single-range behavior.
206
+
207
+ ## Capabilities
208
+
209
+ ```ts
210
+ getStorageCapabilities(storage)
211
+ ```
212
+
213
+ returns native support for:
214
+
215
+ ```text
216
+ streamingRead: true
217
+ streamingWrite: true
218
+ ranges: true
219
+ signedUrls: false
220
+ listing: false
221
+ ```
222
+
223
+ Signed URLs and listing are intentionally deferred to a later storage milestone rather than expanding the `0.1.26` release surface further.
224
+
225
+ ## Overwrite semantics
226
+
227
+ For buffered `put()`, BCP sends a conditional create-only request when `overwrite` is false.
228
+
229
+ Streaming multipart uploads perform a `HeadObject` preflight before starting when overwrite is disabled. That preserves the normal application behavior, but it cannot provide the same atomic create-only guarantee across every S3-compatible multipart implementation if another writer creates the same key between the preflight and completion.
230
+
231
+ Applications that require strict cross-writer serialization should enforce it in their database/business layer or use provider-specific conditional workflows.
232
+
233
+ ## Metadata and ETags
234
+
235
+ S3 response metadata is mapped to `StorageObjectMetadata`:
236
+
237
+ ```ts
238
+ {
239
+ key,
240
+ size,
241
+ contentType,
242
+ lastModified,
243
+ etag,
244
+ checksumSha256?,
245
+ }
246
+ ```
247
+
248
+ Buffered BCP writes persist a SHA-256 value in S3 user metadata. Streaming multipart writes rely on provider metadata/ETag behavior and may not expose `checksumSha256` through the generic contract.
249
+
250
+ Do not assume an S3 ETag is always a plain MD5 checksum; multipart and provider-specific ETag formats can differ.
251
+
252
+ ## Client lifecycle
253
+
254
+ Adapters that create their own AWS SDK client expose:
255
+
256
+ ```ts
257
+ storage.destroy();
258
+ ```
259
+
260
+ Call it during application shutdown when appropriate.
261
+
262
+ When an existing `S3Client` is injected through `client`, BCP does not destroy that externally owned client.
263
+
264
+ ## Security
265
+
266
+ Storage keys are not authorization. Authenticate and authorize a user before allowing access to an object key.
267
+
268
+ Recommended production practices include:
269
+
270
+ - keep buckets private by default,
271
+ - use scoped credentials / IAM policies,
272
+ - do not expose access keys to browser bundles,
273
+ - validate uploaded content independently of MIME metadata,
274
+ - configure provider-side encryption and retention policies when required,
275
+ - keep application upload limits below infrastructure/proxy limits,
276
+ - verify CORS only when browsers intentionally access the bucket directly.