@chidchanun/bcp 0.1.23 → 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.
- package/README.md +658 -578
- package/docs/README.md +317 -318
- package/docs/auth-route-guards.md +14 -0
- package/docs/developer-tools.md +45 -22
- package/docs/file-upload.md +280 -0
- package/docs/releases/0.1.24.md +166 -0
- package/docs/releases/0.1.25.md +132 -0
- package/docs/storage.md +285 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-actions.ts +15 -0
- package/packages/bundler/src/server-production-guards.ts +15 -0
- package/packages/client/src/server.ts +40 -0
- package/packages/server/src/dev-route-graph.ts +91 -0
- package/packages/server/src/file-delivery.ts +629 -0
- package/packages/server/src/middleware-dev-server.ts +217 -9
- package/packages/server/src/storage.ts +829 -0
- package/packages/server/src/upload.ts +556 -0
|
@@ -211,6 +211,20 @@ export function guard() {
|
|
|
211
211
|
}
|
|
212
212
|
```
|
|
213
213
|
|
|
214
|
+
## Standalone production behavior
|
|
215
|
+
|
|
216
|
+
BCP 0.1.24 keeps `bcp/auth` inside the same production guard runtime graph as `runWithRequestContext()`. This guarantees that `requireAuth()`, `requireRole()`, `auth()`, `getSession()` and `cookies()` observe the active request after `bcp build`, rather than resolving through a second framework request-context instance.
|
|
217
|
+
|
|
218
|
+
This fixes the production-only failure:
|
|
219
|
+
|
|
220
|
+
```text
|
|
221
|
+
BCP Framework: server request APIs can only be used while handling a request.
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
No application guard changes are required. Continue importing authentication helpers from `bcp/auth` normally.
|
|
225
|
+
|
|
226
|
+
The production form-action bundle applies the same runtime unification because route guards may execute before an action.
|
|
227
|
+
|
|
214
228
|
## Security boundary
|
|
215
229
|
|
|
216
230
|
`bcp/auth` is server-only. BCP blocks it from page/client graphs and maps its browser export to the server-only runtime guard. Authentication and authorization checks should stay in guards, loaders, actions, API routes, or other server modules.
|
package/docs/developer-tools.md
CHANGED
|
@@ -11,34 +11,55 @@ bcp
|
|
|
11
11
|
bcp-framework
|
|
12
12
|
```
|
|
13
13
|
|
|
14
|
-
`bcp` remains the primary and backward-compatible
|
|
14
|
+
`bcp` remains the primary and backward-compatible executable name inside npm scripts.
|
|
15
15
|
|
|
16
|
-
On Windows, Microsoft SQL Server also installs a native utility named `bcp.exe`. When that executable appears earlier on `PATH`, typing `bcp` in PowerShell can launch the SQL Server utility instead of BCP Framework.
|
|
16
|
+
On Windows, Microsoft SQL Server also installs a native utility named `bcp.exe`. When that executable appears earlier on `PATH`, typing `bcp` directly in PowerShell can launch the SQL Server utility instead of BCP Framework.
|
|
17
17
|
|
|
18
|
-
|
|
18
|
+
PowerShell also does not add an application's `node_modules/.bin` directory to its normal interactive `PATH`. Therefore the recommended interactive Windows invocation is the project-local executable through npm:
|
|
19
19
|
|
|
20
20
|
```powershell
|
|
21
|
-
bcp-framework doctor
|
|
22
|
-
bcp-framework inspect
|
|
23
|
-
bcp-framework dev
|
|
24
|
-
bcp-framework build
|
|
25
|
-
bcp-framework update
|
|
21
|
+
npm exec -- bcp-framework doctor
|
|
22
|
+
npm exec -- bcp-framework inspect
|
|
23
|
+
npm exec -- bcp-framework dev
|
|
24
|
+
npm exec -- bcp-framework build
|
|
25
|
+
npm exec -- bcp-framework update
|
|
26
26
|
```
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Or call the generated Windows shim explicitly:
|
|
29
29
|
|
|
30
30
|
```powershell
|
|
31
|
-
|
|
31
|
+
.\node_modules\.bin\bcp-framework.cmd doctor
|
|
32
32
|
```
|
|
33
33
|
|
|
34
34
|
BCP application npm scripts can continue using `bcp dev`, `bcp build`, and other existing commands because npm places the project's `node_modules/.bin` directory on the script `PATH` before system executables.
|
|
35
35
|
|
|
36
|
+
### Framework source checkout
|
|
37
|
+
|
|
38
|
+
The framework source repository itself is a private development workspace and is not an installed copy of the published package. Merely opening PowerShell in `D:\bcp-framework` does not make `bcp-framework` available on `PATH`.
|
|
39
|
+
|
|
40
|
+
`bcp doctor` and `bcp inspect` are application diagnostics: they expect an application root with `app/` plus an installed BCP dependency. Do not use them as the release-health command for the framework source checkout.
|
|
41
|
+
|
|
42
|
+
For framework development and release validation, use:
|
|
43
|
+
|
|
44
|
+
```powershell
|
|
45
|
+
npm run typecheck
|
|
46
|
+
npm run test:unit
|
|
47
|
+
npm run rc:check
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
After a packed or published package is installed into an application such as `bcp-docs`, run:
|
|
51
|
+
|
|
52
|
+
```powershell
|
|
53
|
+
npm exec -- bcp-framework doctor
|
|
54
|
+
npm exec -- bcp-framework inspect
|
|
55
|
+
```
|
|
56
|
+
|
|
36
57
|
## `bcp doctor`
|
|
37
58
|
|
|
38
|
-
Run a health check from
|
|
59
|
+
Run a health check from an installed application root. On Windows PowerShell, prefer:
|
|
39
60
|
|
|
40
|
-
```
|
|
41
|
-
bcp doctor
|
|
61
|
+
```powershell
|
|
62
|
+
npm exec -- bcp-framework doctor
|
|
42
63
|
```
|
|
43
64
|
|
|
44
65
|
The command checks:
|
|
@@ -106,8 +127,8 @@ The installed folder remains `node_modules/bcp` even when the package inside the
|
|
|
106
127
|
|
|
107
128
|
Use `inspect` when you want a deterministic snapshot of the project inputs BCP sees:
|
|
108
129
|
|
|
109
|
-
```
|
|
110
|
-
bcp inspect
|
|
130
|
+
```powershell
|
|
131
|
+
npm exec -- bcp-framework inspect
|
|
111
132
|
```
|
|
112
133
|
|
|
113
134
|
It prints:
|
|
@@ -126,16 +147,16 @@ It prints:
|
|
|
126
147
|
|
|
127
148
|
Both commands support machine-readable output:
|
|
128
149
|
|
|
129
|
-
```
|
|
130
|
-
bcp doctor --json
|
|
131
|
-
bcp inspect --json
|
|
150
|
+
```powershell
|
|
151
|
+
npm exec -- bcp-framework doctor --json
|
|
152
|
+
npm exec -- bcp-framework inspect --json
|
|
132
153
|
```
|
|
133
154
|
|
|
134
|
-
You can also target another
|
|
155
|
+
You can also target another installed BCP application directory:
|
|
135
156
|
|
|
136
|
-
```
|
|
137
|
-
bcp doctor --root
|
|
138
|
-
bcp inspect --root
|
|
157
|
+
```powershell
|
|
158
|
+
npm exec -- bcp-framework doctor --root ..\my-app --json
|
|
159
|
+
npm exec -- bcp-framework inspect --root ..\my-app --json
|
|
139
160
|
```
|
|
140
161
|
|
|
141
162
|
`--json` is intentionally limited to `doctor` and `inspect` so other CLI commands keep their existing human-oriented output contracts.
|
|
@@ -163,4 +184,6 @@ A simple project-health gate can use:
|
|
|
163
184
|
bcp doctor --json > bcp-doctor.json
|
|
164
185
|
```
|
|
165
186
|
|
|
187
|
+
Inside npm scripts or CI package-script execution, npm already places `node_modules/.bin` on `PATH`.
|
|
188
|
+
|
|
166
189
|
The command exits non-zero when blocking checks fail, while the JSON report remains available for CI logs or artifacts.
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# File Upload
|
|
2
|
+
|
|
3
|
+
BCP Framework 0.1.24 adds server-only multipart and file-storage helpers through `bcp/server`.
|
|
4
|
+
|
|
5
|
+
## Basic API route
|
|
6
|
+
|
|
7
|
+
```ts
|
|
8
|
+
import {
|
|
9
|
+
parseMultipartFormData,
|
|
10
|
+
requireUploadedFile,
|
|
11
|
+
saveUploadedFile,
|
|
12
|
+
} from "bcp/server";
|
|
13
|
+
|
|
14
|
+
export async function POST(
|
|
15
|
+
request: Request
|
|
16
|
+
) {
|
|
17
|
+
const formData =
|
|
18
|
+
await parseMultipartFormData(
|
|
19
|
+
request,
|
|
20
|
+
{
|
|
21
|
+
maxBytes:
|
|
22
|
+
8 * 1024 * 1024,
|
|
23
|
+
}
|
|
24
|
+
);
|
|
25
|
+
|
|
26
|
+
const file =
|
|
27
|
+
requireUploadedFile(
|
|
28
|
+
formData,
|
|
29
|
+
"file",
|
|
30
|
+
{
|
|
31
|
+
maxBytes:
|
|
32
|
+
5 * 1024 * 1024,
|
|
33
|
+
allowedTypes: [
|
|
34
|
+
"image/png",
|
|
35
|
+
"image/jpeg",
|
|
36
|
+
"image/webp",
|
|
37
|
+
],
|
|
38
|
+
allowedExtensions: [
|
|
39
|
+
".png",
|
|
40
|
+
".jpg",
|
|
41
|
+
".jpeg",
|
|
42
|
+
".webp",
|
|
43
|
+
],
|
|
44
|
+
}
|
|
45
|
+
);
|
|
46
|
+
|
|
47
|
+
const saved =
|
|
48
|
+
await saveUploadedFile(
|
|
49
|
+
file,
|
|
50
|
+
{
|
|
51
|
+
directory:
|
|
52
|
+
"./uploads",
|
|
53
|
+
}
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
return Response.json({
|
|
57
|
+
fileName:
|
|
58
|
+
saved.fileName,
|
|
59
|
+
size:
|
|
60
|
+
saved.size,
|
|
61
|
+
checksumSha256:
|
|
62
|
+
saved.checksumSha256,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Request body limit
|
|
68
|
+
|
|
69
|
+
BCP's security gateway enforces the framework-wide request body limit before the request reaches the application server.
|
|
70
|
+
|
|
71
|
+
The default is 1 MiB. Applications that accept larger uploads must explicitly raise it:
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
// bcp.config.ts
|
|
75
|
+
import {
|
|
76
|
+
defineConfig,
|
|
77
|
+
} from "bcp/config";
|
|
78
|
+
|
|
79
|
+
export default defineConfig({
|
|
80
|
+
server: {
|
|
81
|
+
bodyLimit:
|
|
82
|
+
10 * 1024 * 1024,
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
The same setting can be supplied with `BCP_BODY_LIMIT`.
|
|
88
|
+
|
|
89
|
+
Keep `server.bodyLimit` greater than or equal to the multipart limit used by `parseMultipartFormData()`.
|
|
90
|
+
|
|
91
|
+
## Multipart parsing
|
|
92
|
+
|
|
93
|
+
```ts
|
|
94
|
+
const formData =
|
|
95
|
+
await parseMultipartFormData(
|
|
96
|
+
request,
|
|
97
|
+
{
|
|
98
|
+
maxBytes:
|
|
99
|
+
10 * 1024 * 1024,
|
|
100
|
+
}
|
|
101
|
+
);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The helper:
|
|
105
|
+
|
|
106
|
+
- requires `multipart/form-data`,
|
|
107
|
+
- rejects a declared `Content-Length` above `maxBytes`,
|
|
108
|
+
- validates the parsed form payload size,
|
|
109
|
+
- throws `UploadError` with HTTP-oriented status information on validation failures.
|
|
110
|
+
|
|
111
|
+
`maxBytes` is optional. The framework-wide `server.bodyLimit` remains the outer request limit.
|
|
112
|
+
|
|
113
|
+
## Reading files
|
|
114
|
+
|
|
115
|
+
Use `getUploadedFile()` for optional files:
|
|
116
|
+
|
|
117
|
+
```ts
|
|
118
|
+
const avatar =
|
|
119
|
+
getUploadedFile(
|
|
120
|
+
formData,
|
|
121
|
+
"avatar"
|
|
122
|
+
);
|
|
123
|
+
|
|
124
|
+
if (!avatar) {
|
|
125
|
+
// no file supplied
|
|
126
|
+
}
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Use `requireUploadedFile()` when a file is mandatory:
|
|
130
|
+
|
|
131
|
+
```ts
|
|
132
|
+
const avatar =
|
|
133
|
+
requireUploadedFile(
|
|
134
|
+
formData,
|
|
135
|
+
"avatar"
|
|
136
|
+
);
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
A missing required file throws `UploadError` with code `FILE_REQUIRED` and status `400`.
|
|
140
|
+
|
|
141
|
+
## File constraints
|
|
142
|
+
|
|
143
|
+
`getUploadedFile()`, `requireUploadedFile()` and `saveUploadedFile()` can validate:
|
|
144
|
+
|
|
145
|
+
```ts
|
|
146
|
+
{
|
|
147
|
+
maxBytes: 5 * 1024 * 1024,
|
|
148
|
+
allowedTypes: [
|
|
149
|
+
"application/pdf",
|
|
150
|
+
],
|
|
151
|
+
allowedExtensions: [
|
|
152
|
+
".pdf",
|
|
153
|
+
],
|
|
154
|
+
}
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
Supported checks are:
|
|
158
|
+
|
|
159
|
+
- maximum file size,
|
|
160
|
+
- MIME type allowlist,
|
|
161
|
+
- extension allowlist.
|
|
162
|
+
|
|
163
|
+
MIME type and file extension are metadata supplied with the upload. They are useful validation signals but are not content-signature verification. Applications accepting security-sensitive formats should inspect the file content or magic bytes before trusting the file type.
|
|
164
|
+
|
|
165
|
+
## Saving files
|
|
166
|
+
|
|
167
|
+
```ts
|
|
168
|
+
const saved =
|
|
169
|
+
await saveUploadedFile(
|
|
170
|
+
file,
|
|
171
|
+
{
|
|
172
|
+
directory:
|
|
173
|
+
"./uploads/documents",
|
|
174
|
+
}
|
|
175
|
+
);
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
When `fileName` is omitted, BCP generates a UUID-based storage name while preserving a short sanitized extension.
|
|
179
|
+
|
|
180
|
+
The result contains:
|
|
181
|
+
|
|
182
|
+
```ts
|
|
183
|
+
{
|
|
184
|
+
originalName: string;
|
|
185
|
+
fileName: string;
|
|
186
|
+
path: string;
|
|
187
|
+
type: string;
|
|
188
|
+
size: number;
|
|
189
|
+
checksumSha256: string;
|
|
190
|
+
}
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
Files are created with exclusive-write behavior by default. An existing destination produces `FILE_EXISTS` instead of silently overwriting data.
|
|
194
|
+
|
|
195
|
+
Use `overwrite: true` only when replacement is intentional:
|
|
196
|
+
|
|
197
|
+
```ts
|
|
198
|
+
await saveUploadedFile(
|
|
199
|
+
file,
|
|
200
|
+
{
|
|
201
|
+
directory:
|
|
202
|
+
"./uploads",
|
|
203
|
+
fileName:
|
|
204
|
+
"avatar.webp",
|
|
205
|
+
overwrite:
|
|
206
|
+
true,
|
|
207
|
+
}
|
|
208
|
+
);
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## File-name safety
|
|
212
|
+
|
|
213
|
+
`saveUploadedFile()` sanitizes the final file name and verifies that the resolved destination remains inside the configured directory.
|
|
214
|
+
|
|
215
|
+
You can use the same sanitizer independently:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
import {
|
|
219
|
+
sanitizeUploadFileName,
|
|
220
|
+
} from "bcp/server";
|
|
221
|
+
|
|
222
|
+
const safeName =
|
|
223
|
+
sanitizeUploadFileName(
|
|
224
|
+
userFileName
|
|
225
|
+
);
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
Path separators, control characters and operating-system-invalid filename characters are removed or replaced.
|
|
229
|
+
|
|
230
|
+
## Upload errors
|
|
231
|
+
|
|
232
|
+
```ts
|
|
233
|
+
import {
|
|
234
|
+
UploadError,
|
|
235
|
+
} from "bcp/server";
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// upload work
|
|
239
|
+
} catch (error) {
|
|
240
|
+
if (
|
|
241
|
+
error instanceof UploadError
|
|
242
|
+
) {
|
|
243
|
+
return Response.json(
|
|
244
|
+
{
|
|
245
|
+
error: {
|
|
246
|
+
code:
|
|
247
|
+
error.code,
|
|
248
|
+
message:
|
|
249
|
+
error.message,
|
|
250
|
+
},
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
status:
|
|
254
|
+
error.status,
|
|
255
|
+
}
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
throw error;
|
|
260
|
+
}
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
Current error codes include:
|
|
264
|
+
|
|
265
|
+
```text
|
|
266
|
+
INVALID_MULTIPART
|
|
267
|
+
UPLOAD_TOO_LARGE
|
|
268
|
+
FILE_REQUIRED
|
|
269
|
+
FILE_TOO_LARGE
|
|
270
|
+
FILE_TYPE_NOT_ALLOWED
|
|
271
|
+
FILE_EXTENSION_NOT_ALLOWED
|
|
272
|
+
INVALID_FILE_NAME
|
|
273
|
+
FILE_EXISTS
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
## Storage model
|
|
277
|
+
|
|
278
|
+
The 0.1.24 foundation writes to the local filesystem. It does not provide S3/object-storage adapters, multipart streaming directly to object storage, virus scanning or image transcoding yet.
|
|
279
|
+
|
|
280
|
+
For horizontally scaled applications, use shared/object storage rather than relying on instance-local disk.
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
# BCP Framework 0.1.24
|
|
2
|
+
|
|
3
|
+
BCP 0.1.24 introduces the File Upload foundation and includes production/development stabilization fixes found while validating the framework against `bcp-docs`.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- Added multipart upload parsing through `bcp/server`.
|
|
8
|
+
- Added optional and required uploaded-file helpers.
|
|
9
|
+
- Added maximum multipart and per-file size validation.
|
|
10
|
+
- Added MIME type and extension allowlists.
|
|
11
|
+
- Added safe local file persistence with UUID storage names by default.
|
|
12
|
+
- Added filename sanitization and destination traversal protection.
|
|
13
|
+
- Existing files are not overwritten unless `overwrite: true` is explicitly requested.
|
|
14
|
+
- Saved upload metadata includes SHA-256 checksum, original name, stored name, MIME type and byte size.
|
|
15
|
+
- Added `UploadError` with HTTP-oriented status codes and stable upload error codes.
|
|
16
|
+
- Added unit and package-export regression coverage.
|
|
17
|
+
- Fixed development route graph synchronization so page/client bundles rebuild when the actual route topology changes even if the filesystem watcher reports the change as `change` rather than `add`/`unlink`.
|
|
18
|
+
- Fixed standalone auth guards using `requireAuth()` / `requireRole()` after `bcp build` by keeping `bcp/auth` inside the same bundled request-context runtime as the guard evaluator.
|
|
19
|
+
- Applied the same auth-runtime unification to production form-action bundles so guarded actions do not split request context.
|
|
20
|
+
- Added standalone E2E coverage that builds the application and executes a real `requireRole("admin")` guard with a signed session cookie.
|
|
21
|
+
|
|
22
|
+
## Development client-bundle graph fix
|
|
23
|
+
|
|
24
|
+
A development server could previously reach this inconsistent state:
|
|
25
|
+
|
|
26
|
+
```text
|
|
27
|
+
routes contains /docs/[...slug]
|
|
28
|
+
clientBundles does not contain /docs/[...slug]
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
The resulting request failed with:
|
|
32
|
+
|
|
33
|
+
```text
|
|
34
|
+
BCP Framework Error:
|
|
35
|
+
Error: Client bundle was not found for route "/docs/[...slug]".
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Restarting after deleting `.bcp-framework` rebuilt the graph and hid the problem.
|
|
39
|
+
|
|
40
|
+
BCP 0.1.24 tracks the actual page route graph separately at the dev gateway. When pathname, page file or layout topology changes, the internal development server/client bundler is refreshed automatically on the same upstream port. The behavior no longer depends on whether Windows/editor filesystem events are classified as `add`, `unlink` or `change`.
|
|
41
|
+
|
|
42
|
+
## Production auth-guard request context fix
|
|
43
|
+
|
|
44
|
+
A standalone build could previously fail when an application guard imported authentication helpers from `bcp/auth`:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
import {
|
|
48
|
+
requireRole,
|
|
49
|
+
} from "bcp/auth";
|
|
50
|
+
|
|
51
|
+
export async function guard() {
|
|
52
|
+
return requireRole(
|
|
53
|
+
"admin"
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
The production guard evaluator bundled its own `runWithRequestContext()` runtime, but `bcp/auth` could remain an external package import. This created two request-context module instances inside one request path:
|
|
59
|
+
|
|
60
|
+
```text
|
|
61
|
+
guards.mjs request context
|
|
62
|
+
↓
|
|
63
|
+
application guard
|
|
64
|
+
↓
|
|
65
|
+
external bcp/auth
|
|
66
|
+
↓
|
|
67
|
+
second request context
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
The authentication helper then failed while reading cookies with:
|
|
71
|
+
|
|
72
|
+
```text
|
|
73
|
+
BCP Framework: server request APIs can only be used while handling a request.
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
BCP 0.1.24 resolves `bcp/auth` into the guard build graph so `requireAuth()`, `requireRole()`, sessions and cookies share the same active request context as the production guard evaluator. The production action bundle uses the same rule because route guards may execute before server mutations.
|
|
77
|
+
|
|
78
|
+
## Upload example
|
|
79
|
+
|
|
80
|
+
```ts
|
|
81
|
+
import {
|
|
82
|
+
parseMultipartFormData,
|
|
83
|
+
requireUploadedFile,
|
|
84
|
+
saveUploadedFile,
|
|
85
|
+
} from "bcp/server";
|
|
86
|
+
|
|
87
|
+
export async function POST(
|
|
88
|
+
request: Request
|
|
89
|
+
) {
|
|
90
|
+
const formData =
|
|
91
|
+
await parseMultipartFormData(
|
|
92
|
+
request,
|
|
93
|
+
{
|
|
94
|
+
maxBytes:
|
|
95
|
+
8 * 1024 * 1024,
|
|
96
|
+
}
|
|
97
|
+
);
|
|
98
|
+
|
|
99
|
+
const file =
|
|
100
|
+
requireUploadedFile(
|
|
101
|
+
formData,
|
|
102
|
+
"file",
|
|
103
|
+
{
|
|
104
|
+
maxBytes:
|
|
105
|
+
5 * 1024 * 1024,
|
|
106
|
+
allowedTypes: [
|
|
107
|
+
"image/png",
|
|
108
|
+
"image/jpeg",
|
|
109
|
+
"image/webp",
|
|
110
|
+
],
|
|
111
|
+
allowedExtensions: [
|
|
112
|
+
".png",
|
|
113
|
+
".jpg",
|
|
114
|
+
".jpeg",
|
|
115
|
+
".webp",
|
|
116
|
+
],
|
|
117
|
+
}
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
const saved =
|
|
121
|
+
await saveUploadedFile(
|
|
122
|
+
file,
|
|
123
|
+
{
|
|
124
|
+
directory:
|
|
125
|
+
"./uploads",
|
|
126
|
+
}
|
|
127
|
+
);
|
|
128
|
+
|
|
129
|
+
return Response.json(
|
|
130
|
+
saved
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
## Body-limit relationship
|
|
136
|
+
|
|
137
|
+
The existing BCP security gateway still enforces `server.bodyLimit` / `BCP_BODY_LIMIT` before request bodies reach application handlers.
|
|
138
|
+
|
|
139
|
+
The default framework body limit is 1 MiB. Applications accepting larger files must raise the framework body limit as well as choosing appropriate multipart/file limits.
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
export default defineConfig({
|
|
143
|
+
server: {
|
|
144
|
+
bodyLimit:
|
|
145
|
+
10 * 1024 * 1024,
|
|
146
|
+
},
|
|
147
|
+
});
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
## Security model
|
|
151
|
+
|
|
152
|
+
The upload helpers protect storage paths and enforce declared size/type/extension policies, but MIME type and extension do not prove file content. Security-sensitive applications should additionally inspect file signatures/content and run malware scanning when appropriate.
|
|
153
|
+
|
|
154
|
+
The 0.1.24 storage helper targets local filesystem storage. Object-storage adapters and direct streaming are outside this foundation milestone.
|
|
155
|
+
|
|
156
|
+
## Compatibility
|
|
157
|
+
|
|
158
|
+
The upload APIs are additive and server-only. Existing routing, loaders, guards, actions, middleware, authentication, database, validation and logging APIs remain compatible.
|
|
159
|
+
|
|
160
|
+
The dev route-graph fix changes only rebuild behavior when the discovered page topology has actually changed.
|
|
161
|
+
|
|
162
|
+
The production auth-guard change affects bundling only: existing `bcp/auth` application imports and guard APIs remain unchanged.
|
|
163
|
+
|
|
164
|
+
## Next milestone
|
|
165
|
+
|
|
166
|
+
The next planned milestone is BCP 0.1.25 — upload/storage adapters and production file-delivery hardening, unless roadmap priorities are regrouped.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# BCP Framework 0.1.25
|
|
2
|
+
|
|
3
|
+
BCP 0.1.25 adds the first storage-adapter contract and hardened file-delivery responses on top of the multipart/file upload foundation introduced in 0.1.24.
|
|
4
|
+
|
|
5
|
+
## Highlights
|
|
6
|
+
|
|
7
|
+
- Added `StorageAdapter` for application-level storage abstraction.
|
|
8
|
+
- Added built-in `createLocalStorage()` filesystem adapter.
|
|
9
|
+
- Added traversal-safe relative storage keys.
|
|
10
|
+
- Added overwrite protection by default.
|
|
11
|
+
- Added persistent local metadata for content type and SHA-256 identity.
|
|
12
|
+
- Added `stat()`, ranged `read()`, `exists()` and `delete()` operations.
|
|
13
|
+
- Added `storeUploadedFile()` to write validated multipart `File` values into any storage adapter.
|
|
14
|
+
- Added `createStorageResponse()` for production GET/HEAD delivery.
|
|
15
|
+
- Added strong ETag validators for objects written through the local adapter.
|
|
16
|
+
- Added `If-None-Match`, `If-Modified-Since` and `If-Range` support.
|
|
17
|
+
- Added single byte-range delivery with `206 Partial Content`.
|
|
18
|
+
- Added `416 Range Not Satisfiable` handling.
|
|
19
|
+
- Added safe `Content-Disposition` generation for downloads.
|
|
20
|
+
- Added conservative private cache defaults.
|
|
21
|
+
- Added storage/file-delivery unit and public-export regression coverage.
|
|
22
|
+
|
|
23
|
+
## Storage abstraction
|
|
24
|
+
|
|
25
|
+
```ts
|
|
26
|
+
import {
|
|
27
|
+
createLocalStorage,
|
|
28
|
+
} from "bcp/server";
|
|
29
|
+
|
|
30
|
+
const storage =
|
|
31
|
+
createLocalStorage({
|
|
32
|
+
directory:
|
|
33
|
+
"./uploads",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await storage.put(
|
|
37
|
+
"documents/report.txt",
|
|
38
|
+
"Hello",
|
|
39
|
+
{
|
|
40
|
+
contentType:
|
|
41
|
+
"text/plain",
|
|
42
|
+
}
|
|
43
|
+
);
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
The public adapter contract includes:
|
|
47
|
+
|
|
48
|
+
```text
|
|
49
|
+
put
|
|
50
|
+
stat
|
|
51
|
+
read
|
|
52
|
+
exists
|
|
53
|
+
delete
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Application code can depend on `StorageAdapter` instead of the local filesystem implementation.
|
|
57
|
+
|
|
58
|
+
## Upload integration
|
|
59
|
+
|
|
60
|
+
```ts
|
|
61
|
+
const stored =
|
|
62
|
+
await storeUploadedFile(
|
|
63
|
+
file,
|
|
64
|
+
{
|
|
65
|
+
storage,
|
|
66
|
+
key:
|
|
67
|
+
"avatars/user-101.webp",
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
The upload validation APIs from 0.1.24 remain compatible. `storeUploadedFile()` is additive and reuses the same `UploadConstraints` validation model.
|
|
73
|
+
|
|
74
|
+
## File delivery
|
|
75
|
+
|
|
76
|
+
```ts
|
|
77
|
+
return createStorageResponse(
|
|
78
|
+
request,
|
|
79
|
+
storage,
|
|
80
|
+
"documents/report.pdf",
|
|
81
|
+
{
|
|
82
|
+
disposition:
|
|
83
|
+
"attachment",
|
|
84
|
+
downloadName:
|
|
85
|
+
"report.pdf",
|
|
86
|
+
}
|
|
87
|
+
);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
The helper supports:
|
|
91
|
+
|
|
92
|
+
```text
|
|
93
|
+
200 full response
|
|
94
|
+
206 single byte range
|
|
95
|
+
304 validator hit
|
|
96
|
+
404 missing object
|
|
97
|
+
405 unsupported request method
|
|
98
|
+
416 invalid/unsupported range
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
`HEAD` returns the same entity metadata headers without reading object bytes into the response body.
|
|
102
|
+
|
|
103
|
+
Multiple ranges are intentionally unsupported in 0.1.25 and return `416`.
|
|
104
|
+
|
|
105
|
+
## Cache policy
|
|
106
|
+
|
|
107
|
+
The default is:
|
|
108
|
+
|
|
109
|
+
```text
|
|
110
|
+
private, max-age=0, must-revalidate
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Applications can explicitly opt public immutable files into stronger caching.
|
|
114
|
+
|
|
115
|
+
## Security model
|
|
116
|
+
|
|
117
|
+
- Storage keys reject absolute paths and traversal segments.
|
|
118
|
+
- BCP's local metadata directory cannot be addressed as an application storage key.
|
|
119
|
+
- Existing objects are not overwritten unless requested explicitly.
|
|
120
|
+
- Download filenames are stripped of path/control characters before being placed in response headers.
|
|
121
|
+
- Storage keys do not provide authorization; route/API guards must still enforce ownership and access permissions.
|
|
122
|
+
- Content type remains metadata, not content-signature verification.
|
|
123
|
+
|
|
124
|
+
## Compatibility
|
|
125
|
+
|
|
126
|
+
`saveUploadedFile()` from 0.1.24 remains supported. Applications can migrate incrementally to `StorageAdapter` / `storeUploadedFile()` when they need backend-independent storage behavior.
|
|
127
|
+
|
|
128
|
+
The route-graph synchronization and standalone auth-guard fixes completed during the 0.1.24 stabilization cycle remain part of the 0.1.25 baseline.
|
|
129
|
+
|
|
130
|
+
## Next milestone
|
|
131
|
+
|
|
132
|
+
The next planned milestone is BCP 0.1.26 — storage adapter ecosystem / S3-compatible integration and production upload streaming, unless roadmap priorities are regrouped.
|