@chidchanun/bcp 0.1.23 → 0.1.24

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.
@@ -11,34 +11,55 @@ bcp
11
11
  bcp-framework
12
12
  ```
13
13
 
14
- `bcp` remains the primary and backward-compatible command.
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
- Use the Windows-safe alias in that situation:
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
- You can also explicitly run the project-local executable with npm:
28
+ Or call the generated Windows shim explicitly:
29
29
 
30
30
  ```powershell
31
- npm exec -- bcp doctor
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 the application root:
59
+ Run a health check from an installed application root. On Windows PowerShell, prefer:
39
60
 
40
- ```bash
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
- ```bash
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
- ```bash
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 project directory:
155
+ You can also target another installed BCP application directory:
135
156
 
136
- ```bash
137
- bcp doctor --root ../my-app --json
138
- bcp inspect --root ../my-app --json
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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.1.23",
3
+ "version": "0.1.24",
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",
@@ -131,6 +131,11 @@ export async function buildProductionActions(
131
131
  frameworkDirectory,
132
132
  "../../client/src/server.ts"
133
133
  );
134
+ const frameworkAuthEntry =
135
+ path.resolve(
136
+ frameworkDirectory,
137
+ "../../client/src/auth.ts"
138
+ );
134
139
  const frameworkServerOnlyEntry =
135
140
  path.resolve(
136
141
  frameworkDirectory,
@@ -187,6 +192,16 @@ export async function buildProductionActions(
187
192
  frameworkServerEntry,
188
193
  })
189
194
  );
195
+ buildApi.onResolve(
196
+ {
197
+ filter:
198
+ /^bcp\/auth$/,
199
+ },
200
+ () => ({
201
+ path:
202
+ frameworkAuthEntry,
203
+ })
204
+ );
190
205
  buildApi.onResolve(
191
206
  {
192
207
  filter:
@@ -118,6 +118,11 @@ export async function buildProductionGuards(
118
118
  frameworkDirectory,
119
119
  "../../client/src/server.ts"
120
120
  );
121
+ const frameworkAuthEntry =
122
+ path.resolve(
123
+ frameworkDirectory,
124
+ "../../client/src/auth.ts"
125
+ );
121
126
  const frameworkServerOnlyEntry =
122
127
  path.resolve(
123
128
  frameworkDirectory,
@@ -174,6 +179,16 @@ export async function buildProductionGuards(
174
179
  frameworkServerEntry,
175
180
  })
176
181
  );
182
+ buildApi.onResolve(
183
+ {
184
+ filter:
185
+ /^bcp\/auth$/,
186
+ },
187
+ () => ({
188
+ path:
189
+ frameworkAuthEntry,
190
+ })
191
+ );
177
192
  buildApi.onResolve(
178
193
  {
179
194
  filter:
@@ -30,6 +30,22 @@ export {
30
30
  type LoggerOptions,
31
31
  } from "../../server/src/logger.js";
32
32
 
33
+ export {
34
+ getUploadedFile,
35
+ parseMultipartFormData,
36
+ requireUploadedFile,
37
+ sanitizeUploadFileName,
38
+ saveUploadedFile,
39
+ UploadError,
40
+ validateUploadedFile,
41
+
42
+ type MultipartUploadOptions,
43
+ type SaveUploadedFileOptions,
44
+ type SavedUploadedFile,
45
+ type UploadConstraints,
46
+ type UploadErrorCode,
47
+ } from "../../server/src/upload.js";
48
+
33
49
  export {
34
50
  json,
35
51
  redirect,