@chidchanun/bcp 0.1.24 → 0.1.25

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,285 @@
1
+ # Storage and File Delivery
2
+
3
+ BCP Framework `0.1.25` adds a server-only storage abstraction and production file-delivery helpers through `bcp/server`.
4
+
5
+ The initial adapter is local filesystem storage. The `StorageAdapter` contract keeps application code independent from the local implementation so later object-storage adapters can implement the same surface.
6
+
7
+ ## Create local storage
8
+
9
+ ```ts
10
+ import {
11
+ createLocalStorage,
12
+ } from "bcp/server";
13
+
14
+ export const uploads =
15
+ createLocalStorage({
16
+ directory:
17
+ "./uploads",
18
+ });
19
+ ```
20
+
21
+ Storage keys are relative forward-slash paths:
22
+
23
+ ```text
24
+ avatars/user-101.webp
25
+ documents/2026/report.pdf
26
+ ```
27
+
28
+ Absolute paths, `.` / `..` traversal segments and BCP's reserved metadata directory are rejected.
29
+
30
+ ## Write and read objects
31
+
32
+ ```ts
33
+ const stored =
34
+ await uploads.put(
35
+ "notes/hello.txt",
36
+ "Hello BCP",
37
+ {
38
+ contentType:
39
+ "text/plain",
40
+ }
41
+ );
42
+
43
+ const bytes =
44
+ await uploads.read(
45
+ stored.key
46
+ );
47
+ ```
48
+
49
+ `put()` does not overwrite an existing object unless `overwrite: true` is explicitly supplied.
50
+
51
+ The local adapter stores metadata separately from the object bytes so content type and SHA-256 identity survive process restarts.
52
+
53
+ Object metadata contains:
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.
67
+
68
+ ## Storage adapter contract
69
+
70
+ ```ts
71
+ interface StorageAdapter {
72
+ put(
73
+ key: string,
74
+ value: StorageWriteValue,
75
+ options?: StoragePutOptions
76
+ ): Promise<StorageObjectMetadata>;
77
+
78
+ stat(
79
+ key: string
80
+ ): Promise<StorageObjectMetadata | null>;
81
+
82
+ read(
83
+ key: string,
84
+ options?: StorageReadOptions
85
+ ): Promise<Uint8Array>;
86
+
87
+ exists(
88
+ key: string
89
+ ): Promise<boolean>;
90
+
91
+ delete(
92
+ key: string
93
+ ): Promise<boolean>;
94
+ }
95
+ ```
96
+
97
+ `read()` supports inclusive byte ranges through `start` and `end`.
98
+
99
+ ## Upload directly into storage
100
+
101
+ BCP 0.1.24's multipart parser and validation helpers remain unchanged. `0.1.25` adds `storeUploadedFile()` as the bridge into a storage adapter.
102
+
103
+ ```ts
104
+ import {
105
+ createLocalStorage,
106
+ parseMultipartFormData,
107
+ requireUploadedFile,
108
+ storeUploadedFile,
109
+ } from "bcp/server";
110
+
111
+ const uploads =
112
+ createLocalStorage({
113
+ directory:
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
157
+ );
158
+ }
159
+ ```
160
+
161
+ If `key` is omitted, BCP creates a UUID-based storage key while preserving a sanitized extension.
162
+
163
+ ## Deliver a stored file
164
+
165
+ Use `createStorageResponse()` from a GET/HEAD API route:
166
+
167
+ ```ts
168
+ import {
169
+ createLocalStorage,
170
+ createStorageResponse,
171
+ } from "bcp/server";
172
+
173
+ const uploads =
174
+ createLocalStorage({
175
+ directory:
176
+ "./uploads",
177
+ });
178
+
179
+ export function GET(
180
+ request: Request,
181
+ {
182
+ params,
183
+ }: {
184
+ params: {
185
+ key: string;
186
+ };
187
+ }
188
+ ) {
189
+ return createStorageResponse(
190
+ request,
191
+ uploads,
192
+ params.key,
193
+ {
194
+ cacheControl:
195
+ "private, max-age=3600",
196
+ }
197
+ );
198
+ }
199
+ ```
200
+
201
+ The response helper supports:
202
+
203
+ - `GET` and `HEAD`.
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.
214
+
215
+ Multiple byte ranges are intentionally not supported in this milestone. They return `416` rather than generating a multipart range response.
216
+
217
+ ## Downloads
218
+
219
+ ```ts
220
+ return createStorageResponse(
221
+ request,
222
+ uploads,
223
+ "reports/monthly.pdf",
224
+ {
225
+ disposition:
226
+ "attachment",
227
+ downloadName:
228
+ "monthly-report.pdf",
229
+ }
230
+ );
231
+ ```
232
+
233
+ BCP strips path and control characters from the header filename and also emits an RFC 5987 UTF-8 filename parameter.
234
+
235
+ ## Cache defaults
236
+
237
+ The default file response policy is conservative:
238
+
239
+ ```text
240
+ Cache-Control: private, max-age=0, must-revalidate
241
+ ```
242
+
243
+ Public immutable files must opt in explicitly, for example:
244
+
245
+ ```ts
246
+ {
247
+ cacheControl:
248
+ "public, max-age=31536000, immutable",
249
+ }
250
+ ```
251
+
252
+ Do not mark authorization-protected or user-private files as public.
253
+
254
+ ## Errors
255
+
256
+ Storage operations throw `StorageError` with a stable code and HTTP-oriented status:
257
+
258
+ ```ts
259
+ import {
260
+ StorageError,
261
+ } from "bcp/server";
262
+ ```
263
+
264
+ Current codes:
265
+
266
+ ```text
267
+ INVALID_KEY
268
+ OBJECT_NOT_FOUND
269
+ OBJECT_EXISTS
270
+ RANGE_NOT_SATISFIABLE
271
+ ```
272
+
273
+ `createStorageResponse()` converts ordinary missing objects and invalid HTTP ranges into `404` / `416` responses directly.
274
+
275
+ ## Security boundary
276
+
277
+ Storage keys are not authorization. Always perform authentication/authorization before passing a user-selected key to the storage layer.
278
+
279
+ The local adapter prevents traversal outside its configured directory, but applications are still responsible for deciding which authenticated user can read, overwrite or delete each logical object.
280
+
281
+ MIME types are metadata and are not file-signature verification. Continue validating upload content when the threat model requires it.
282
+
283
+ ## Adapter scope
284
+
285
+ BCP 0.1.25 ships only `createLocalStorage()` as a built-in adapter. S3-compatible/cloud adapters are expected to implement `StorageAdapter` without changing application upload and delivery code.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.1.24",
3
+ "version": "0.1.25",
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",
@@ -46,6 +46,30 @@ export {
46
46
  type UploadErrorCode,
47
47
  } from "../../server/src/upload.js";
48
48
 
49
+ export {
50
+ createLocalStorage,
51
+ normalizeStorageKey,
52
+ StorageError,
53
+ storeUploadedFile,
54
+
55
+ type LocalStorageOptions,
56
+ type StorageAdapter,
57
+ type StorageErrorCode,
58
+ type StorageObjectMetadata,
59
+ type StoragePutOptions,
60
+ type StorageReadOptions,
61
+ type StorageWriteValue,
62
+ type StoredUploadedObject,
63
+ type StoreUploadedFileOptions,
64
+ } from "../../server/src/storage.js";
65
+
66
+ export {
67
+ createStorageResponse,
68
+ isStorageRangeError,
69
+
70
+ type StorageResponseOptions,
71
+ } from "../../server/src/file-delivery.js";
72
+
49
73
  export {
50
74
  json,
51
75
  redirect,