@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.
@@ -0,0 +1,434 @@
1
+ # Storage Ecosystem
2
+
3
+ BCP Framework `0.1.27` expands the `0.1.25` storage abstraction and `0.1.26` streaming/cloud work into a broader object-storage ecosystem.
4
+
5
+ > Version target: `0.1.27` unreleased development target until RC validation and publication complete.
6
+
7
+ The public APIs are available through `bcp/server`.
8
+
9
+ ## Capabilities
10
+
11
+ Use `getStorageEcosystemCapabilities()` when code needs to select an optimized path:
12
+
13
+ ```ts
14
+ import {
15
+ getStorageEcosystemCapabilities,
16
+ } from "bcp/server";
17
+
18
+ const capabilities =
19
+ getStorageEcosystemCapabilities(
20
+ storage
21
+ );
22
+ ```
23
+
24
+ The result includes the original storage capabilities plus the `0.1.27` ecosystem surface:
25
+
26
+ ```ts
27
+ {
28
+ streamingRead: boolean;
29
+ streamingWrite: boolean;
30
+ ranges: boolean;
31
+ signedUrls: boolean;
32
+ listing: boolean;
33
+ signedReadUrls: boolean;
34
+ signedWriteUrls: boolean;
35
+ copy: boolean;
36
+ move: boolean;
37
+ metadata: boolean;
38
+ bulkDelete: boolean;
39
+ }
40
+ ```
41
+
42
+ Built-in local storage supports listing, copy, move, metadata and bulk deletion. It intentionally does not provide signed URLs.
43
+
44
+ Built-in S3-compatible storage supports the complete capability set.
45
+
46
+ ## List objects
47
+
48
+ ```ts
49
+ import {
50
+ listStorageObjects,
51
+ } from "bcp/server";
52
+
53
+ const page =
54
+ await listStorageObjects(
55
+ storage,
56
+ {
57
+ prefix:
58
+ "documents/",
59
+ limit:
60
+ 50,
61
+ }
62
+ );
63
+ ```
64
+
65
+ Result:
66
+
67
+ ```ts
68
+ {
69
+ objects: StorageObjectMetadata[];
70
+ cursor?: string;
71
+ }
72
+ ```
73
+
74
+ Use the cursor returned by the previous page:
75
+
76
+ ```ts
77
+ const next =
78
+ await listStorageObjects(
79
+ storage,
80
+ {
81
+ prefix:
82
+ "documents/",
83
+ limit:
84
+ 50,
85
+ cursor:
86
+ page.cursor,
87
+ }
88
+ );
89
+ ```
90
+
91
+ Cursors are opaque provider/application tokens. Do not decode them or persist assumptions about their internal format.
92
+
93
+ `limit` defaults to `100` and accepts values from `1` through `1000`.
94
+
95
+ ### Prefix behavior
96
+
97
+ Both of these are valid:
98
+
99
+ ```ts
100
+ prefix: "documents"
101
+ prefix: "documents/"
102
+ ```
103
+
104
+ A trailing slash is preserved when supplied, which is useful when application keys are organized as logical folders.
105
+
106
+ Storage listing is not a filesystem directory API. Keys remain object keys, and consumers should not infer authorization from prefixes.
107
+
108
+ ## Copy objects
109
+
110
+ ```ts
111
+ import {
112
+ copyStorageObject,
113
+ } from "bcp/server";
114
+
115
+ const copied =
116
+ await copyStorageObject(
117
+ storage,
118
+ "incoming/report.pdf",
119
+ "archive/report.pdf"
120
+ );
121
+ ```
122
+
123
+ By default an existing destination is rejected with `StorageError` code `OBJECT_EXISTS`.
124
+
125
+ Intentional replacement must opt in:
126
+
127
+ ```ts
128
+ await copyStorageObject(
129
+ storage,
130
+ source,
131
+ destination,
132
+ {
133
+ overwrite:
134
+ true,
135
+ }
136
+ );
137
+ ```
138
+
139
+ S3-compatible storage uses native `CopyObject`.
140
+
141
+ When a custom adapter does not expose a native `copy()` method, the helper can fall back to streamed read/write copy using the existing `StorageAdapter` APIs.
142
+
143
+ If the adapter supports user metadata, the compatibility copy path preserves it.
144
+
145
+ ## Move objects
146
+
147
+ ```ts
148
+ import {
149
+ moveStorageObject,
150
+ } from "bcp/server";
151
+
152
+ await moveStorageObject(
153
+ storage,
154
+ "tmp/avatar.webp",
155
+ "users/42/avatar.webp"
156
+ );
157
+ ```
158
+
159
+ A move is implemented as a successful copy followed by deletion of the source.
160
+
161
+ This has an important distributed-storage implication: copy and delete are not one atomic transaction across generic object stores. If the delete fails after the copy succeeds, both source and destination can temporarily exist.
162
+
163
+ Application workflows that need stronger business-level atomicity should coordinate the move with database state rather than assuming the storage operation is transactional.
164
+
165
+ ## User metadata
166
+
167
+ Portable application metadata is represented as:
168
+
169
+ ```ts
170
+ Record<string, string>
171
+ ```
172
+
173
+ Store metadata with an object:
174
+
175
+ ```ts
176
+ const stored =
177
+ await storage.put(
178
+ "documents/report.pdf",
179
+ bytes,
180
+ {
181
+ contentType:
182
+ "application/pdf",
183
+ metadata: {
184
+ owner:
185
+ "user-42",
186
+ category:
187
+ "report",
188
+ },
189
+ }
190
+ );
191
+ ```
192
+
193
+ The same option is available on the ecosystem `putStream()` contract.
194
+
195
+ Read metadata independently:
196
+
197
+ ```ts
198
+ import {
199
+ getStorageMetadata,
200
+ } from "bcp/server";
201
+
202
+ const metadata =
203
+ await getStorageMetadata(
204
+ storage,
205
+ stored.key
206
+ );
207
+ ```
208
+
209
+ Replace it:
210
+
211
+ ```ts
212
+ import {
213
+ setStorageMetadata,
214
+ } from "bcp/server";
215
+
216
+ await setStorageMetadata(
217
+ storage,
218
+ stored.key,
219
+ {
220
+ owner:
221
+ "user-42",
222
+ status:
223
+ "approved",
224
+ }
225
+ );
226
+ ```
227
+
228
+ `setStorageMetadata()` replaces the application metadata map rather than merging it.
229
+
230
+ ### Metadata rules
231
+
232
+ BCP normalizes metadata keys to lowercase.
233
+
234
+ Keys must use a portable subset:
235
+
236
+ ```text
237
+ letters
238
+ numbers
239
+ underscore
240
+ hyphen
241
+ dot
242
+ ```
243
+
244
+ The framework reserves:
245
+
246
+ ```text
247
+ bcp_sha256
248
+ ```
249
+
250
+ for internal checksum metadata.
251
+
252
+ Metadata values must be strings and cannot contain CR/LF or NUL characters.
253
+
254
+ Metadata is useful for lightweight object context, but it should not replace application authorization or relational data that needs querying/transactions.
255
+
256
+ ## Local metadata implementation
257
+
258
+ Local storage keeps BCP-internal and application metadata under the existing reserved metadata directory rather than mixing it with user-visible object keys.
259
+
260
+ The reserved directory is excluded from object listings.
261
+
262
+ Deleting or replacing an object also clears or replaces its application metadata as appropriate.
263
+
264
+ ## S3 metadata implementation
265
+
266
+ S3-compatible storage maps application metadata to S3 user metadata.
267
+
268
+ BCP preserves its internal `bcp_sha256` metadata when application metadata is replaced.
269
+
270
+ Updating S3 object metadata uses S3's copy-to-self metadata replacement behavior. This means metadata mutation is a provider object operation, not merely a local in-memory change.
271
+
272
+ ## Bulk deletion
273
+
274
+ ```ts
275
+ import {
276
+ deleteStorageObjects,
277
+ } from "bcp/server";
278
+
279
+ const result =
280
+ await deleteStorageObjects(
281
+ storage,
282
+ [
283
+ "tmp/a.bin",
284
+ "tmp/b.bin",
285
+ "tmp/c.bin",
286
+ ]
287
+ );
288
+ ```
289
+
290
+ Result:
291
+
292
+ ```ts
293
+ {
294
+ deleted: string[];
295
+ missing: string[];
296
+ failed: Array<{
297
+ key: string;
298
+ message: string;
299
+ }>;
300
+ }
301
+ ```
302
+
303
+ Duplicate input keys are normalized to one deletion request.
304
+
305
+ Local/custom fallback deletion can distinguish missing keys.
306
+
307
+ S3's delete-object semantics treat a delete for a missing key as successful, so S3-compatible bulk deletion normally reports those keys as `deleted` rather than `missing`.
308
+
309
+ S3 requests are batched to the provider's 1000-object multi-delete limit.
310
+
311
+ ## Signed read URLs
312
+
313
+ Signed URLs are intended primarily for object-storage adapters such as S3/R2/MinIO.
314
+
315
+ ```ts
316
+ import {
317
+ createStorageSignedReadUrl,
318
+ } from "bcp/server";
319
+
320
+ const url =
321
+ await createStorageSignedReadUrl(
322
+ storage,
323
+ "videos/demo.mp4",
324
+ {
325
+ expiresIn:
326
+ 5 * 60,
327
+ }
328
+ );
329
+ ```
330
+
331
+ The URL lets a client fetch the object directly from the object-storage endpoint for the configured lifetime.
332
+
333
+ BCP defaults to 15 minutes and accepts an expiry from 1 second through 7 days.
334
+
335
+ ## Signed write URLs
336
+
337
+ ```ts
338
+ import {
339
+ createStorageSignedWriteUrl,
340
+ } from "bcp/server";
341
+
342
+ const url =
343
+ await createStorageSignedWriteUrl(
344
+ storage,
345
+ "uploads/video.mp4",
346
+ {
347
+ expiresIn:
348
+ 5 * 60,
349
+ contentType:
350
+ "video/mp4",
351
+ metadata: {
352
+ owner:
353
+ "user-42",
354
+ },
355
+ }
356
+ );
357
+ ```
358
+
359
+ A common direct-upload flow is:
360
+
361
+ ```text
362
+ Browser
363
+ ↓ request upload authorization
364
+ BCP application
365
+ ↓ returns short-lived signed URL
366
+ Browser
367
+ ↓ PUT bytes directly
368
+ S3 / R2 / MinIO
369
+ ```
370
+
371
+ This avoids proxying very large file bytes through the BCP application server.
372
+
373
+ If the signed request includes content type or metadata headers, the direct uploader must send the corresponding headers required by the generated signature/provider behavior.
374
+
375
+ ## Signed URL security
376
+
377
+ A signed URL is a temporary credential. Anyone who obtains a valid URL can normally perform the signed operation until it expires.
378
+
379
+ Recommended practices:
380
+
381
+ - generate signed URLs only after authorization,
382
+ - use short expirations,
383
+ - choose server-generated object keys instead of trusting arbitrary client paths,
384
+ - limit the signed operation to one object/action,
385
+ - do not log full signed URLs when query strings contain credentials,
386
+ - keep buckets private by default,
387
+ - configure bucket CORS only for intended browser origins/methods,
388
+ - persist upload ownership in application data before or immediately after issuing upload authorization.
389
+
390
+ ## Unsupported operations
391
+
392
+ A helper that requires an adapter capability which cannot be provided generically throws `StorageEcosystemError`:
393
+
394
+ ```text
395
+ code: UNSUPPORTED_OPERATION
396
+ status: 501
397
+ ```
398
+
399
+ For example, local filesystem storage does not fabricate a signed URL API.
400
+
401
+ Other ecosystem validation errors include:
402
+
403
+ ```text
404
+ INVALID_CURSOR
405
+ INVALID_METADATA
406
+ ```
407
+
408
+ ## Compatibility model
409
+
410
+ `StorageAdapter` itself remains the stable minimum contract from earlier releases.
411
+
412
+ `StorageEcosystemAdapter` layers optional capabilities on top of it. This allows older custom adapters to continue compiling while applications gradually adopt richer operations.
413
+
414
+ Prefer the public helper functions when code should work across adapters:
415
+
416
+ ```text
417
+ listStorageObjects
418
+ copyStorageObject
419
+ moveStorageObject
420
+ deleteStorageObjects
421
+ getStorageMetadata
422
+ setStorageMetadata
423
+ createStorageSignedReadUrl
424
+ createStorageSignedWriteUrl
425
+ ```
426
+
427
+ Using the helpers also gives the framework a place to provide compatibility fallbacks when an operation can be implemented safely through the base adapter contract.
428
+
429
+ ## Related documentation
430
+
431
+ - [Storage and File Delivery](storage.md)
432
+ - [S3-Compatible Storage](s3-storage.md)
433
+ - [File Upload](file-upload.md)
434
+ - [BCP Framework 0.1.27](releases/0.1.27.md)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.1.26",
3
+ "version": "0.1.28",
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",
@@ -81,6 +81,7 @@
81
81
  "dependencies": {
82
82
  "@aws-sdk/client-s3": "^3.1119.0",
83
83
  "@aws-sdk/lib-storage": "^3.1119.0",
84
+ "@aws-sdk/s3-request-presigner": "^3.1121.0",
84
85
  "@babel/core": "^8.0.1",
85
86
  "@babel/preset-react": "^8.0.1",
86
87
  "@babel/preset-typescript": "^8.0.1",
@@ -30,6 +30,15 @@ export {
30
30
  type LoggerOptions,
31
31
  } from "../../server/src/logger.js";
32
32
 
33
+ export {
34
+ getProductionHardeningConfig,
35
+ registerShutdownHook,
36
+
37
+ type ProductionHardeningConfig,
38
+ type ShutdownHook,
39
+ type ShutdownHookOptions,
40
+ } from "../../server/src/production-hardening.js";
41
+
33
42
  export {
34
43
  getUploadedFile,
35
44
  parseMultipartFormData,
@@ -55,7 +64,6 @@ export {
55
64
  } from "../../server/src/upload-stream.js";
56
65
 
57
66
  export {
58
- createLocalStorage,
59
67
  getStorageCapabilities,
60
68
  normalizeStorageKey,
61
69
  putStorageStream,
@@ -78,13 +86,45 @@ export {
78
86
  } from "../../server/src/storage.js";
79
87
 
80
88
  export {
81
- createS3Storage,
89
+ copyStorageObject,
90
+ createLocalStorage,
91
+ createStorageSignedReadUrl,
92
+ createStorageSignedWriteUrl,
93
+ deleteStorageObjects,
94
+ getStorageEcosystemCapabilities,
95
+ getStorageMetadata,
96
+ listStorageObjects,
97
+ moveStorageObject,
98
+ setStorageMetadata,
99
+ StorageEcosystemError,
100
+
101
+ type StorageCopyOptions,
102
+ type StorageDeleteManyFailure,
103
+ type StorageDeleteManyResult,
104
+ type StorageEcosystemAdapter,
105
+ type StorageEcosystemCapabilities,
106
+ type StorageEcosystemErrorCode,
107
+ type StorageEcosystemPutOptions,
108
+ type StorageEcosystemPutStreamOptions,
109
+ type StorageListOptions,
110
+ type StorageListResult,
111
+ type StorageSignedUrlOptions,
112
+ type StorageSignedWriteUrlOptions,
113
+ type StorageUserMetadata,
114
+ } from "../../server/src/storage-ecosystem.js";
82
115
 
116
+ export {
83
117
  type S3StorageAdapter,
84
118
  type S3StorageMultipartOptions,
85
119
  type S3StorageOptions,
86
120
  } from "../../server/src/storage-s3.js";
87
121
 
122
+ export {
123
+ createS3Storage,
124
+
125
+ type S3StorageEcosystemAdapter,
126
+ } from "../../server/src/storage-s3-ecosystem.js";
127
+
88
128
  export {
89
129
  createStorageResponse,
90
130
  isStorageRangeError,