@chidchanun/bcp 0.1.16 → 0.1.18

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.
@@ -1,58 +1,520 @@
1
1
  # Middleware
2
2
 
3
+ BCP Middleware runs before application routing and can inspect requests, rewrite URLs, redirect requests, change request headers, return responses directly, and add response headers.
4
+
5
+ BCP 0.1.18 adds Middleware System v2 while keeping the v1 API backward-compatible.
6
+
7
+ ## Create middleware
8
+
3
9
  Create `middleware.ts` in the project root.
4
10
 
11
+ ```text
12
+ my-app/
13
+ ├─ app/
14
+ ├─ middleware.ts
15
+ ├─ bcp.config.ts
16
+ └─ package.json
17
+ ```
18
+
19
+ Import middleware APIs from `bcp/middleware`.
20
+
21
+ ## Middleware System v2
22
+
23
+ Middleware v2 uses an onion-style pipeline. A handler can run code before the next handler/application, call `await next()`, then inspect or change the final `Response` on the way back out.
24
+
5
25
  ```ts
6
- import {
7
- next,
8
- redirect,
9
- rewrite,
10
- type MiddlewareConfig,
11
- type MiddlewareRequest,
26
+ import type {
27
+ MiddlewarePipelineHandler,
12
28
  } from "bcp/middleware";
13
29
 
14
- export const config: MiddlewareConfig = {
30
+ export const middleware:
31
+ MiddlewarePipelineHandler =
32
+ async (
33
+ request,
34
+ next,
35
+ context
36
+ ) => {
37
+ const started =
38
+ performance.now();
39
+
40
+ context.state.requestPath =
41
+ request.nextUrl.pathname;
42
+
43
+ const response =
44
+ await next();
45
+
46
+ response.headers.set(
47
+ "X-Response-Time",
48
+ `${Math.round(
49
+ performance.now() -
50
+ started
51
+ )}ms`
52
+ );
53
+
54
+ return response;
55
+ };
56
+ ```
57
+
58
+ `await next()` executes the rest of the middleware chain and then the BCP application. The returned value is the real downstream Web `Response`.
59
+
60
+ This makes response interception possible without creating an extra API abstraction.
61
+
62
+ ## Middleware chains
63
+
64
+ Export an array to compose several middleware handlers.
65
+
66
+ ```ts
67
+ import type {
68
+ MiddlewarePipelineHandler,
69
+ } from "bcp/middleware";
70
+
71
+ const logger:
72
+ MiddlewarePipelineHandler =
73
+ async (
74
+ request,
75
+ next
76
+ ) => {
77
+ console.log(
78
+ "request",
79
+ request.method,
80
+ request.nextUrl.pathname
81
+ );
82
+
83
+ const response =
84
+ await next();
85
+
86
+ console.log(
87
+ "response",
88
+ response.status
89
+ );
90
+
91
+ return response;
92
+ };
93
+
94
+ const responseHeaders:
95
+ MiddlewarePipelineHandler =
96
+ async (
97
+ _request,
98
+ next
99
+ ) => {
100
+ const response =
101
+ await next();
102
+
103
+ response.headers.set(
104
+ "X-App",
105
+ "BCP"
106
+ );
107
+
108
+ return response;
109
+ };
110
+
111
+ export const middleware = [
112
+ logger,
113
+ responseHeaders,
114
+ ];
115
+ ```
116
+
117
+ Execution order is:
118
+
119
+ ```text
120
+ Request
121
+
122
+ logger before
123
+
124
+ responseHeaders before
125
+
126
+ BCP router / page / API
127
+
128
+ responseHeaders after
129
+
130
+ logger after
131
+
132
+ Response
133
+ ```
134
+
135
+ A middleware handler may call `next()` only once. Calling it more than once throws an error because a request body and downstream application execution cannot safely be replayed automatically.
136
+
137
+ ## Shared request state
138
+
139
+ The third argument is a `MiddlewareContext` shared by every handler in one request.
140
+
141
+ ```ts
142
+ import type {
143
+ MiddlewarePipelineHandler,
144
+ } from "bcp/middleware";
145
+
146
+ const identify:
147
+ MiddlewarePipelineHandler =
148
+ async (
149
+ request,
150
+ next,
151
+ context
152
+ ) => {
153
+ context.state.requestSource =
154
+ request.headers.get(
155
+ "X-Request-Source"
156
+ ) ??
157
+ "web";
158
+
159
+ return next();
160
+ };
161
+
162
+ const observe:
163
+ MiddlewarePipelineHandler =
164
+ async (
165
+ _request,
166
+ next,
167
+ context
168
+ ) => {
169
+ console.log(
170
+ context.state.requestSource
171
+ );
172
+
173
+ return next();
174
+ };
175
+
176
+ export const middleware = [
177
+ identify,
178
+ observe,
179
+ ];
180
+ ```
181
+
182
+ `context.state` starts empty for every request and is not shared across concurrent requests.
183
+
184
+ Use it for lightweight request-scoped information such as timing metadata, correlation IDs, feature flags, or values computed by an earlier middleware handler.
185
+
186
+ Do not treat it as persistent storage.
187
+
188
+ ## Matchers
189
+
190
+ Use `config.matcher` to limit the routes that execute middleware.
191
+
192
+ ```ts
193
+ import type {
194
+ MiddlewareConfig,
195
+ } from "bcp/middleware";
196
+
197
+ export const config:
198
+ MiddlewareConfig = {
15
199
  matcher: [
16
200
  "/dashboard/:path*",
17
201
  "/api/private/:path*",
18
202
  ],
19
203
  };
204
+ ```
205
+
206
+ String matchers support:
207
+
208
+ ```text
209
+ /dashboard
210
+ /users/:id
211
+ /dashboard/:path*
212
+ ```
213
+
214
+ A JavaScript `RegExp` is also supported.
215
+
216
+ ## Request data
217
+
218
+ The middleware request contains:
219
+
220
+ ```ts
221
+ request.url
222
+ request.method
223
+ request.headers
224
+ request.nextUrl
225
+ request.cookies
226
+ ```
227
+
228
+ Example:
229
+
230
+ ```ts
231
+ export async function middleware(
232
+ request,
233
+ next
234
+ ) {
235
+ const theme =
236
+ request.cookies.get(
237
+ "theme"
238
+ );
239
+
240
+ console.log(
241
+ request.method,
242
+ request.nextUrl.pathname,
243
+ theme
244
+ );
245
+
246
+ return next();
247
+ }
248
+ ```
249
+
250
+ ## Response interception
251
+
252
+ Because `next()` returns the downstream `Response`, middleware can add or change headers after a page or API route finishes.
253
+
254
+ ```ts
255
+ export async function middleware(
256
+ request,
257
+ next
258
+ ) {
259
+ const response =
260
+ await next();
261
+
262
+ response.headers.set(
263
+ "X-Route",
264
+ request.nextUrl.pathname
265
+ );
266
+
267
+ return response;
268
+ }
269
+ ```
270
+
271
+ Middleware may also replace the response completely:
272
+
273
+ ```ts
274
+ export async function middleware(
275
+ request,
276
+ next
277
+ ) {
278
+ const response =
279
+ await next();
280
+
281
+ if (
282
+ response.status === 404 &&
283
+ request.nextUrl.pathname
284
+ .startsWith("/api/")
285
+ ) {
286
+ return Response.json(
287
+ {
288
+ error:
289
+ "API endpoint not found",
290
+ },
291
+ {
292
+ status: 404,
293
+ }
294
+ );
295
+ }
296
+
297
+ return response;
298
+ }
299
+ ```
300
+
301
+ ## Rewrite
302
+
303
+ `rewrite()` keeps the browser URL but changes the route processed by BCP.
304
+
305
+ ```ts
306
+ import {
307
+ rewrite,
308
+ } from "bcp/middleware";
309
+
310
+ export function middleware(
311
+ request
312
+ ) {
313
+ if (
314
+ request.nextUrl.pathname ===
315
+ "/old-dashboard"
316
+ ) {
317
+ return rewrite(
318
+ "/dashboard"
319
+ );
320
+ }
321
+ }
322
+ ```
323
+
324
+ Rewrites must remain on the same origin.
325
+
326
+ In a v2 chain, return `rewrite()` before calling `next()`.
327
+
328
+ ## Redirect
329
+
330
+ ```ts
331
+ import {
332
+ redirect,
333
+ } from "bcp/middleware";
334
+
335
+ export function middleware(
336
+ request
337
+ ) {
338
+ if (
339
+ !request.cookies.has(
340
+ "session"
341
+ )
342
+ ) {
343
+ return redirect(
344
+ "/login",
345
+ {
346
+ status: 303,
347
+ }
348
+ );
349
+ }
350
+ }
351
+ ```
352
+
353
+ Supported redirect statuses are `301`, `302`, `303`, `307`, and `308`.
354
+
355
+ Middleware redirects continue to work with direct document requests and BCP SPA navigation.
356
+
357
+ ## Request and response headers
358
+
359
+ The existing v1 helpers continue to work inside a v2 chain.
360
+
361
+ ```ts
362
+ import {
363
+ next,
364
+ } from "bcp/middleware";
365
+
366
+ export function middleware() {
367
+ return next({
368
+ request: {
369
+ headers: {
370
+ "X-Internal-Request":
371
+ "true",
372
+ },
373
+ },
374
+ response: {
375
+ headers: {
376
+ "X-Powered-By-App":
377
+ "BCP",
378
+ },
379
+ },
380
+ });
381
+ }
382
+ ```
383
+
384
+ Request headers are visible to later middleware and the application. Response headers are applied after downstream execution.
385
+
386
+ ## Direct responses
387
+
388
+ A middleware handler can stop the chain by returning a Web `Response` without calling `next()`.
389
+
390
+ ```ts
391
+ export function middleware(
392
+ request
393
+ ) {
394
+ if (
395
+ request.nextUrl.pathname ===
396
+ "/maintenance"
397
+ ) {
398
+ return new Response(
399
+ "Maintenance",
400
+ {
401
+ status: 503,
402
+ }
403
+ );
404
+ }
405
+ }
406
+ ```
407
+
408
+ ## Error middleware pattern
409
+
410
+ A v2 handler can wrap downstream execution with `try/catch`.
411
+
412
+ ```ts
413
+ export async function middleware(
414
+ request,
415
+ next
416
+ ) {
417
+ try {
418
+ return await next();
419
+ } catch (error) {
420
+ console.error(
421
+ "Request failed",
422
+ request.nextUrl.pathname,
423
+ error
424
+ );
425
+
426
+ return Response.json(
427
+ {
428
+ error:
429
+ "Internal Server Error",
430
+ },
431
+ {
432
+ status: 500,
433
+ }
434
+ );
435
+ }
436
+ }
437
+ ```
438
+
439
+ Use this for application-specific error translation. Framework security and server error boundaries still have their own responsibilities.
440
+
441
+ ## v1 compatibility
442
+
443
+ BCP 0.1.18 does not require existing middleware to be rewritten.
444
+
445
+ A v1 handler remains valid:
446
+
447
+ ```ts
448
+ import {
449
+ next,
450
+ redirect,
451
+ } from "bcp/middleware";
20
452
 
21
453
  export function middleware(
22
- request: MiddlewareRequest
454
+ request
23
455
  ) {
24
456
  if (
25
- !request.cookies.has("session")
457
+ request.nextUrl.pathname ===
458
+ "/private" &&
459
+ !request.cookies.has(
460
+ "session"
461
+ )
26
462
  ) {
27
- return redirect("/login");
463
+ return redirect(
464
+ "/login"
465
+ );
28
466
  }
29
467
 
30
468
  return next();
31
469
  }
32
470
  ```
33
471
 
34
- ## Results
472
+ The important difference is:
35
473
 
36
- Middleware may:
474
+ | Capability | v1 | v2 |
475
+ | --- | --- | --- |
476
+ | Inspect request | Yes | Yes |
477
+ | Match routes | Yes | Yes |
478
+ | Redirect | Yes | Yes |
479
+ | Rewrite | Yes | Yes |
480
+ | Request/response header helpers | Yes | Yes |
481
+ | Return direct `Response` | Yes | Yes |
482
+ | Multiple middleware handlers | No | Yes |
483
+ | `await next()` executes downstream | No | Yes |
484
+ | Inspect final route response | No | Yes |
485
+ | Shared request context | No | Yes |
486
+ | Before/after onion flow | No | Yes |
37
487
 
38
- - continue with `next()`
39
- - rewrite to another same-origin route with `rewrite()`
40
- - redirect to an HTTP(S) URL with `redirect()`
41
- - return a `Response` directly
42
- - mutate request and response headers through middleware options
488
+ Existing v1 middleware can be migrated gradually. A project may mix legacy one-argument handlers and v2 handlers in the same exported array.
43
489
 
44
- ## Matchers
490
+ ## Execution model
45
491
 
46
- Matchers support exact paths, dynamic path segments and catch-all patterns such as:
492
+ At runtime, middleware sits in front of the BCP application.
47
493
 
48
494
  ```text
49
- /dashboard
50
- /users/:id
51
- /dashboard/:path*
495
+ Browser
496
+
497
+ BCP outer gateways
498
+
499
+ Middleware v2 pipeline
500
+
501
+ Actions / Guards / Loaders / Pages / APIs
502
+
503
+ Middleware v2 response path
504
+
505
+ Browser
52
506
  ```
53
507
 
508
+ Development and standalone production use the same middleware pipeline semantics.
509
+
510
+ ## Performance note
511
+
512
+ Middleware response interception crosses a Web `Response` boundary so BCP can expose the completed downstream response to user middleware. Avoid unnecessary body replacement for large responses when only a header change is needed.
513
+
54
514
  ## Security constraints
55
515
 
56
- Rewrites must stay on the same origin. Redirects and rewrites reject unsupported URL protocols and embedded URL credentials. Header values ultimately pass through Node.js response validation, while framework configuration rejects CR/LF/null-byte injection in security header settings.
516
+ Rewrites must stay on the same origin. Redirects and rewrites reject unsupported URL protocols and embedded URL credentials.
517
+
518
+ Framework-internal `/_bcp/*` requests bypass user middleware where required by the runtime. BCP SPA navigation still evaluates middleware against the intended destination URL rather than exposing internal navigation transport details to application policy.
57
519
 
58
- Framework-internal `/_bcp/*` requests bypass user middleware execution where required by the runtime.
520
+ Do not use middleware as a replacement for route authorization when authorization is page-specific. Use route guards and `bcp/auth` helpers such as `requireAuth()` or `requireRole()` for protected application areas.
@@ -0,0 +1,36 @@
1
+ # BCP Framework 0.1.17
2
+
3
+ ## Auth + Route Guard Integration
4
+
5
+ BCP Framework 0.1.17 connects the Authentication Core introduced in 0.1.16 with the existing scoped route guard system.
6
+
7
+ ### New `bcp/auth` guard helpers
8
+
9
+ - `requireAuth()` verifies the active BCP auth session and returns it as guard data.
10
+ - `requireRole()` verifies authentication plus one or more roles/permissions.
11
+ - `createAuthGuard()` creates a guard function that can be exported directly from `guard.ts`.
12
+ - `createRoleGuard()` creates a role-protected guard function.
13
+ - `getGuardAuth()` safely reads typed auth data from merged `guardData`.
14
+
15
+ Successful auth helpers return the session under `guardData.auth`, so child guards and page loaders can reuse the authenticated user without verifying the cookie again.
16
+
17
+ ### Unauthorized and forbidden behavior
18
+
19
+ - Unauthenticated requests redirect to `/login` with HTTP `303` by default.
20
+ - `redirectTo: null` returns `401 Unauthorized` instead.
21
+ - Authenticated users that fail a role requirement receive `403 Forbidden` by default.
22
+ - `forbiddenRedirectTo` can redirect insufficient-role users to a custom page.
23
+
24
+ ### Role matching
25
+
26
+ - A single role can be required with `requireRole("admin")`.
27
+ - Multiple roles default to `match: "any"`.
28
+ - `match: "all"` requires every requested value.
29
+ - `roleField` allows permission arrays or custom role fields such as `permissions` instead of the default `role` field.
30
+
31
+ ### Reliability
32
+
33
+ - Added unit coverage for authentication redirects, 401 mode, role allow/deny behavior, custom role fields and guard factories.
34
+ - Added page-guard pipeline integration coverage proving auth data flows from a parent auth guard into child guards.
35
+ - Added publish-surface regression coverage for the route guard helpers exposed through `bcp/auth`.
36
+ - Added dedicated Auth Route Guards documentation.
@@ -0,0 +1,119 @@
1
+ # BCP Framework 0.1.18
2
+
3
+ BCP Framework 0.1.18 introduces Middleware System v2 while preserving existing Middleware v1 applications.
4
+
5
+ ## Middleware System v2
6
+
7
+ Middleware can now execute as an onion-style chain around the application.
8
+
9
+ ```ts
10
+ export async function middleware(
11
+ request,
12
+ next,
13
+ context
14
+ ) {
15
+ const started =
16
+ performance.now();
17
+
18
+ const response =
19
+ await next();
20
+
21
+ response.headers.set(
22
+ "X-Response-Time",
23
+ `${Math.round(
24
+ performance.now() -
25
+ started
26
+ )}ms`
27
+ );
28
+
29
+ return response;
30
+ }
31
+ ```
32
+
33
+ `await next()` runs the remaining middleware and the real downstream BCP route, then returns the resulting Web `Response`.
34
+
35
+ ## Middleware chains
36
+
37
+ Applications may export an array of handlers:
38
+
39
+ ```ts
40
+ export const middleware = [
41
+ logger,
42
+ requestContext,
43
+ responseHeaders,
44
+ ];
45
+ ```
46
+
47
+ Handlers execute from outer to inner before the application, then unwind from inner to outer after the application returns.
48
+
49
+ ## Shared request context
50
+
51
+ The third handler argument exposes request-scoped state:
52
+
53
+ ```ts
54
+ context.state.requestId =
55
+ "request-123";
56
+ ```
57
+
58
+ The same state object is visible to later middleware in that request and is recreated for the next request.
59
+
60
+ ## Response interception
61
+
62
+ Middleware can now inspect and modify the actual route response after page/API processing finishes.
63
+
64
+ This supports patterns such as:
65
+
66
+ - request/response timing
67
+ - response metadata
68
+ - application-specific response headers
69
+ - error translation
70
+ - response status observation
71
+ - composable logging
72
+
73
+ ## Compatibility
74
+
75
+ Middleware v1 remains supported.
76
+
77
+ Existing code such as:
78
+
79
+ ```ts
80
+ export function middleware(
81
+ request
82
+ ) {
83
+ return next();
84
+ }
85
+ ```
86
+
87
+ continues to work without modification.
88
+
89
+ Legacy `next()`, `rewrite()`, `redirect()`, matchers, request headers, response headers, cookie reads and direct `Response` returns remain available.
90
+
91
+ A project can also mix v1 and v2 handlers in the same middleware array.
92
+
93
+ ## Runtime
94
+
95
+ The middleware proxy now exposes the real downstream application response to the v2 pipeline in both:
96
+
97
+ - development
98
+ - standalone production
99
+
100
+ SPA navigation keeps middleware redirect/rewrite behavior while evaluating middleware policy against the intended destination URL.
101
+
102
+ ## Reliability
103
+
104
+ Added regression coverage for:
105
+
106
+ - onion execution order
107
+ - response interception
108
+ - shared request context
109
+ - context isolation across requests
110
+ - legacy handlers inside v2 chains
111
+ - rewrite propagation
112
+ - duplicate `next()` rejection
113
+ - middleware array validation
114
+
115
+ ## Documentation
116
+
117
+ `docs/middleware.md` now contains the complete Middleware v2 guide and v1 migration reference.
118
+
119
+ `docs/README.md` provides a documentation content map, recommended navigation, feature inventory, and source-of-truth guidance for building the `bcp-docs` website.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@chidchanun/bcp",
3
- "version": "0.1.16",
3
+ "version": "0.1.18",
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",