@chidchanun/bcp 0.1.9 → 0.1.11
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/CHANGELOG.md +83 -1
- package/README.md +263 -8
- package/docs/form-actions.md +314 -0
- package/docs/releasing.md +34 -19
- package/docs/route-guards.md +240 -0
- package/docs/updating.md +130 -0
- package/package.json +1 -1
- package/packages/bundler/src/server-production-actions.ts +523 -0
- package/packages/bundler/src/server-production-guards.ts +497 -0
- package/packages/bundler/src/server-production-middleware.ts +50 -8
- package/packages/cli/src/args.ts +58 -0
- package/packages/cli/src/index.ts +41 -13
- package/packages/cli/src/update.ts +860 -0
- package/packages/client/src/form.tsx +548 -0
- package/packages/client/src/index.tsx +11 -0
- package/packages/client/src/loader-data.tsx +171 -41
- package/packages/client/src/server.ts +6 -0
- package/packages/server/src/form-action-dev-proxy.ts +601 -0
- package/packages/server/src/form-action-transport.ts +271 -0
- package/packages/server/src/form-action.ts +493 -0
- package/packages/server/src/middleware-dev-server.ts +56 -2
- package/packages/server/src/page-guard.ts +529 -0
- package/packages/server/src/page-loader.ts +317 -20
- package/packages/server/src/standalone-production-runtime-v2-action.ts +765 -0
- package/packages/server/src/standalone-production-runtime-v2-guard.ts +815 -0
- package/packages/server/src/standalone-production-runtime-v3.ts +1 -1
|
@@ -0,0 +1,314 @@
|
|
|
1
|
+
# Form Actions and Server Mutations
|
|
2
|
+
|
|
3
|
+
BCP form actions provide a route-scoped server mutation primitive for application forms without requiring an API route for every create, update or delete operation.
|
|
4
|
+
|
|
5
|
+
This feature is intentionally separate from future BCP Server Actions. In this release, client pages do **not** import a server function directly. A page submits a named action such as `"saveUser"`, and BCP resolves that name from the sibling `actions.ts` / `actions.tsx` file on the server.
|
|
6
|
+
|
|
7
|
+
## Route convention
|
|
8
|
+
|
|
9
|
+
```text
|
|
10
|
+
app/users/[id]/
|
|
11
|
+
├─ guard.ts # optional
|
|
12
|
+
├─ loader.ts # optional read path
|
|
13
|
+
├─ actions.ts # server-only mutation path
|
|
14
|
+
└─ page.tsx
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
Only one of `actions.ts` or `actions.tsx` may exist next to a page.
|
|
18
|
+
|
|
19
|
+
## Defining an action
|
|
20
|
+
|
|
21
|
+
```ts
|
|
22
|
+
// app/users/[id]/actions.ts
|
|
23
|
+
import "bcp/server-only";
|
|
24
|
+
|
|
25
|
+
import {
|
|
26
|
+
cookies,
|
|
27
|
+
redirect,
|
|
28
|
+
type PageActionContext,
|
|
29
|
+
} from "bcp/server";
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
revalidatePath,
|
|
33
|
+
revalidateTag,
|
|
34
|
+
} from "bcp/cache";
|
|
35
|
+
|
|
36
|
+
export async function saveUser(
|
|
37
|
+
formData: FormData,
|
|
38
|
+
context: PageActionContext
|
|
39
|
+
) {
|
|
40
|
+
const name =
|
|
41
|
+
String(
|
|
42
|
+
formData.get("name") ?? ""
|
|
43
|
+
).trim();
|
|
44
|
+
|
|
45
|
+
// Perform the database mutation here.
|
|
46
|
+
|
|
47
|
+
const cookieStore =
|
|
48
|
+
await cookies();
|
|
49
|
+
|
|
50
|
+
cookieStore.set(
|
|
51
|
+
"last-user",
|
|
52
|
+
context.params.id,
|
|
53
|
+
{
|
|
54
|
+
httpOnly: true,
|
|
55
|
+
sameSite: "lax",
|
|
56
|
+
path: "/",
|
|
57
|
+
}
|
|
58
|
+
);
|
|
59
|
+
|
|
60
|
+
revalidatePath("/users");
|
|
61
|
+
revalidateTag("users");
|
|
62
|
+
|
|
63
|
+
return {
|
|
64
|
+
ok: true,
|
|
65
|
+
id: context.params.id,
|
|
66
|
+
name,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export async function removeUser(
|
|
71
|
+
_formData: FormData,
|
|
72
|
+
context: PageActionContext
|
|
73
|
+
) {
|
|
74
|
+
// Delete from the database.
|
|
75
|
+
|
|
76
|
+
revalidatePath("/users");
|
|
77
|
+
|
|
78
|
+
return redirect(
|
|
79
|
+
"/users",
|
|
80
|
+
303
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
An action receives:
|
|
86
|
+
|
|
87
|
+
- `formData` — the submitted browser `FormData`.
|
|
88
|
+
- `context.params` — matched dynamic route parameters.
|
|
89
|
+
- `context.searchParams` — a fresh `URLSearchParams` for the target page URL.
|
|
90
|
+
- `context.guardData` — merged data from route guards that ran before the action.
|
|
91
|
+
- `context.method` — the semantic mutation method: `POST`, `PUT`, `PATCH`, or `DELETE`.
|
|
92
|
+
|
|
93
|
+
The normal `bcp/server` request context is active while the action runs, so `cookies()`, `headers()`, `requestUrl()`, `requestMethod()`, `requestId()`, `getSession()` and other server helpers work directly. `requestMethod()` reflects the semantic action method rather than the internal POST transport.
|
|
94
|
+
|
|
95
|
+
## Rendering a form
|
|
96
|
+
|
|
97
|
+
```tsx
|
|
98
|
+
"use client";
|
|
99
|
+
|
|
100
|
+
import {
|
|
101
|
+
Form,
|
|
102
|
+
useActionData,
|
|
103
|
+
useActionError,
|
|
104
|
+
useFormStatus,
|
|
105
|
+
} from "bcp";
|
|
106
|
+
|
|
107
|
+
interface SaveResult {
|
|
108
|
+
ok: boolean;
|
|
109
|
+
id: string;
|
|
110
|
+
name: string;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export default function UserEditor() {
|
|
114
|
+
return (
|
|
115
|
+
<Form
|
|
116
|
+
action="saveUser"
|
|
117
|
+
method="patch"
|
|
118
|
+
>
|
|
119
|
+
<input
|
|
120
|
+
name="name"
|
|
121
|
+
required
|
|
122
|
+
/>
|
|
123
|
+
|
|
124
|
+
<SubmitButton />
|
|
125
|
+
<Result />
|
|
126
|
+
</Form>
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
function SubmitButton() {
|
|
131
|
+
const status =
|
|
132
|
+
useFormStatus();
|
|
133
|
+
|
|
134
|
+
return (
|
|
135
|
+
<button
|
|
136
|
+
type="submit"
|
|
137
|
+
disabled={status.pending}
|
|
138
|
+
>
|
|
139
|
+
{status.pending
|
|
140
|
+
? "Saving..."
|
|
141
|
+
: "Save"}
|
|
142
|
+
</button>
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function Result() {
|
|
147
|
+
const data =
|
|
148
|
+
useActionData<SaveResult>();
|
|
149
|
+
const error =
|
|
150
|
+
useActionError();
|
|
151
|
+
|
|
152
|
+
if (error) {
|
|
153
|
+
return (
|
|
154
|
+
<p role="alert">
|
|
155
|
+
{error.message}
|
|
156
|
+
</p>
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (!data) {
|
|
161
|
+
return null;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return (
|
|
165
|
+
<p>
|
|
166
|
+
Saved {data.name}
|
|
167
|
+
</p>
|
|
168
|
+
);
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Supported mutation methods
|
|
173
|
+
|
|
174
|
+
`<Form>` supports:
|
|
175
|
+
|
|
176
|
+
```tsx
|
|
177
|
+
<Form action="createUser" method="post" />
|
|
178
|
+
<Form action="replaceUser" method="put" />
|
|
179
|
+
<Form action="updateUser" method="patch" />
|
|
180
|
+
<Form action="deleteUser" method="delete" />
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
Browsers only support GET and POST as native HTML form methods. BCP therefore uses POST as the wire transport for progressive enhancement and preserves the intended method separately. The action context and `requestMethod()` still report `PUT`, `PATCH`, or `DELETE` as requested.
|
|
184
|
+
|
|
185
|
+
GET is deliberately not an action method. Reads belong in page loaders or API GET handlers.
|
|
186
|
+
|
|
187
|
+
## Action return values
|
|
188
|
+
|
|
189
|
+
A mutation may return either:
|
|
190
|
+
|
|
191
|
+
1. JSON-safe action data.
|
|
192
|
+
2. `undefined`, which BCP normalizes to `null`.
|
|
193
|
+
3. A Web `Response`, commonly `redirect()`.
|
|
194
|
+
|
|
195
|
+
Serializable action data follows the same safety model as loader data: primitives, arrays and plain objects are allowed. Functions, symbols, BigInt, non-finite numbers, Date/Map/Set/class instances and circular references are rejected before transport.
|
|
196
|
+
|
|
197
|
+
For validation-style UI in this release, return a serializable object such as:
|
|
198
|
+
|
|
199
|
+
```ts
|
|
200
|
+
return {
|
|
201
|
+
ok: false,
|
|
202
|
+
fieldErrors: {
|
|
203
|
+
email: "Email is required",
|
|
204
|
+
},
|
|
205
|
+
};
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Typed validation helpers and CSRF-specific APIs are planned as a separate security/validation milestone.
|
|
209
|
+
|
|
210
|
+
## Pending, data, and transport errors
|
|
211
|
+
|
|
212
|
+
Inside a `<Form>` subtree:
|
|
213
|
+
|
|
214
|
+
- `useFormStatus()` exposes `pending`, `action`, `method`, and the current transport/server error.
|
|
215
|
+
- `useActionData<T>()` exposes the most recent serializable action result.
|
|
216
|
+
- `useActionError()` exposes the most recent thrown transport/runtime error.
|
|
217
|
+
|
|
218
|
+
Submitting the form again clears the previous action data before the new request begins.
|
|
219
|
+
|
|
220
|
+
## Redirects
|
|
221
|
+
|
|
222
|
+
Actions can return a normal BCP redirect:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
return redirect(
|
|
226
|
+
"/users",
|
|
227
|
+
303
|
|
228
|
+
);
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
With JavaScript enabled, BCP converts the redirect into an action transport payload and continues with client navigation. Without JavaScript, the browser receives the normal HTTP redirect.
|
|
232
|
+
|
|
233
|
+
Cookies set before the redirect are preserved in both paths.
|
|
234
|
+
|
|
235
|
+
## Revalidation and refreshing loaders
|
|
236
|
+
|
|
237
|
+
`revalidatePath()` and `revalidateTag()` can be called directly inside an action to invalidate BCP data cache entries:
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
revalidatePath("/users");
|
|
241
|
+
revalidateTag("users");
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
If the current page should immediately rerun its loader after a successful data-returning action, enable the form's `refresh` option:
|
|
245
|
+
|
|
246
|
+
```tsx
|
|
247
|
+
<Form
|
|
248
|
+
action="saveUser"
|
|
249
|
+
method="patch"
|
|
250
|
+
refresh
|
|
251
|
+
>
|
|
252
|
+
...
|
|
253
|
+
</Form>
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`refresh` uses the existing BCP router refresh path after the action result is received. Redirecting actions do not need `refresh`.
|
|
257
|
+
|
|
258
|
+
## Progressive enhancement
|
|
259
|
+
|
|
260
|
+
The rendered HTML form posts to BCP's internal action endpoint using a normal browser POST. The target page is recovered from the same-origin referrer when JavaScript is unavailable.
|
|
261
|
+
|
|
262
|
+
When an action returns serializable data and no explicit `Response`, the no-JavaScript path uses POST/Redirect/GET with HTTP `303` back to the original target page. This prevents duplicate browser resubmission on refresh.
|
|
263
|
+
|
|
264
|
+
With JavaScript enabled, `<Form>` intercepts the submit and sends the same `FormData` through the action transport without reloading the document.
|
|
265
|
+
|
|
266
|
+
## Middleware, guards, and request order
|
|
267
|
+
|
|
268
|
+
For a mutation, the effective server order is:
|
|
269
|
+
|
|
270
|
+
```text
|
|
271
|
+
Security gateway
|
|
272
|
+
↓
|
|
273
|
+
Project middleware
|
|
274
|
+
↓
|
|
275
|
+
Resolve target page
|
|
276
|
+
↓
|
|
277
|
+
Route guards (root → child)
|
|
278
|
+
↓
|
|
279
|
+
Named action
|
|
280
|
+
↓
|
|
281
|
+
Response cookies / redirect / data
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
A guard redirect prevents the action from running. Guard data is passed into `context.guardData`, so an authorization lookup does not need to be repeated in the action.
|
|
285
|
+
|
|
286
|
+
## Why action names are strings
|
|
287
|
+
|
|
288
|
+
This release intentionally uses:
|
|
289
|
+
|
|
290
|
+
```tsx
|
|
291
|
+
<Form action="saveUser">
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
and not:
|
|
295
|
+
|
|
296
|
+
```tsx
|
|
297
|
+
<Form action={saveUser}>
|
|
298
|
+
```
|
|
299
|
+
|
|
300
|
+
Passing a server function through a client bundle requires a compiler-generated server reference, serialization protocol and action identifier system. That belongs to the later BCP Server Actions milestone. Keeping named route actions separate now prevents server/database modules from being pulled into hydrated client graphs.
|
|
301
|
+
|
|
302
|
+
## When to use an API route instead
|
|
303
|
+
|
|
304
|
+
Use a form action when the mutation belongs to a BCP page UI.
|
|
305
|
+
|
|
306
|
+
Keep an API route when the caller is an external client such as:
|
|
307
|
+
|
|
308
|
+
- a mobile application,
|
|
309
|
+
- another backend service,
|
|
310
|
+
- a webhook provider,
|
|
311
|
+
- Postman/API consumers,
|
|
312
|
+
- a public or versioned HTTP API.
|
|
313
|
+
|
|
314
|
+
A project can use both patterns: loaders for page reads, guards for authorization, form actions for page-owned mutations, and API routes for external HTTP contracts.
|
package/docs/releasing.md
CHANGED
|
@@ -18,7 +18,7 @@ Application source continues importing from `bcp`. `create-bcp-app` stores the s
|
|
|
18
18
|
Use the version helper instead of editing package metadata manually:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
|
-
npm run version:set -- 0.1.
|
|
21
|
+
npm run version:set -- 0.1.10
|
|
22
22
|
```
|
|
23
23
|
|
|
24
24
|
It synchronizes the release version across:
|
|
@@ -187,24 +187,29 @@ and the executable:
|
|
|
187
187
|
bcp
|
|
188
188
|
```
|
|
189
189
|
|
|
190
|
-
A generated application's `package.json`
|
|
190
|
+
A newly generated application's `package.json` pins the selected framework release exactly so a later plain package-manager install cannot silently move BCP to a different release:
|
|
191
191
|
|
|
192
192
|
```json
|
|
193
193
|
{
|
|
194
|
+
"scripts": {
|
|
195
|
+
"update": "bcp update"
|
|
196
|
+
},
|
|
194
197
|
"dependencies": {
|
|
195
|
-
"bcp": "npm:@chidchanun/bcp
|
|
198
|
+
"bcp": "npm:@chidchanun/bcp@0.1.10"
|
|
196
199
|
}
|
|
197
200
|
}
|
|
198
201
|
```
|
|
199
202
|
|
|
203
|
+
The explicit updater is responsible for resolving and installing later framework releases.
|
|
204
|
+
|
|
200
205
|
## 8. Create the release tag
|
|
201
206
|
|
|
202
207
|
Only after `npm run rc:check` passes and `CHANGELOG.md` is ready:
|
|
203
208
|
|
|
204
209
|
```bash
|
|
205
210
|
git status
|
|
206
|
-
git tag -a v0.1.
|
|
207
|
-
git push origin v0.1.
|
|
211
|
+
git tag -a v0.1.10 -m "BCP Framework v0.1.10"
|
|
212
|
+
git push origin v0.1.10
|
|
208
213
|
```
|
|
209
214
|
|
|
210
215
|
Use the actual version from `package.json` in the tag.
|
|
@@ -231,19 +236,13 @@ The command refuses to publish unless all of these conditions are true:
|
|
|
231
236
|
- staged package names and versions match the selected release
|
|
232
237
|
- the target version has not already been accepted by npm
|
|
233
238
|
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
```text
|
|
237
|
-
next
|
|
238
|
-
```
|
|
239
|
-
|
|
240
|
-
For `1.x` and later it defaults to:
|
|
239
|
+
Stable BCP releases use this npm dist-tag by default, including stable `0.x` releases:
|
|
241
240
|
|
|
242
241
|
```text
|
|
243
242
|
latest
|
|
244
243
|
```
|
|
245
244
|
|
|
246
|
-
|
|
245
|
+
Use `BCP_DIST_TAG` only when intentionally publishing a separate channel such as `next` or `beta`:
|
|
247
246
|
|
|
248
247
|
```bash
|
|
249
248
|
BCP_DIST_TAG=beta npm run release:publish:yes
|
|
@@ -258,6 +257,8 @@ npm run release:publish:yes
|
|
|
258
257
|
|
|
259
258
|
The framework publishes first. After `npm publish` succeeds, the release script accepts either normal version visibility or the selected dist-tag pointing at the new version. This prevents npm registry/security-processing delays from being misclassified as a failed publish.
|
|
260
259
|
|
|
260
|
+
Using `latest` for stable releases is also part of the updater contract: `bcp update` resolves `@chidchanun/bcp@latest` by default.
|
|
261
|
+
|
|
261
262
|
## 10. Recover from a partial publish
|
|
262
263
|
|
|
263
264
|
If the framework package was accepted by npm but publishing `create-bcp-app` failed, fix the external issue without changing that release commit or tag, then use:
|
|
@@ -270,18 +271,32 @@ Resume mode intentionally does not run `release:version-check` or `npm publish -
|
|
|
270
271
|
|
|
271
272
|
Do not use resume mode to overwrite or replace an existing npm version; npm versions are immutable.
|
|
272
273
|
|
|
273
|
-
## 11. Install the
|
|
274
|
+
## 11. Install or update the stable release
|
|
275
|
+
|
|
276
|
+
The recommended new-project path is:
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
npx create-bcp-app@latest my-app
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
A generated application keeps the documented `bcp` import name through an npm alias and pins the selected framework version exactly.
|
|
283
|
+
|
|
284
|
+
For an existing project already on an updater-capable release:
|
|
285
|
+
|
|
286
|
+
```bash
|
|
287
|
+
npm run update
|
|
288
|
+
```
|
|
274
289
|
|
|
275
|
-
|
|
290
|
+
For a project on BCP 0.1.9 or older, bootstrap the updater once with:
|
|
276
291
|
|
|
277
292
|
```bash
|
|
278
|
-
npx
|
|
293
|
+
npx @chidchanun/bcp@latest update
|
|
279
294
|
```
|
|
280
295
|
|
|
281
|
-
|
|
296
|
+
A manual install that preserves the `bcp` import name is:
|
|
282
297
|
|
|
283
298
|
```bash
|
|
284
|
-
npm install bcp@npm:@chidchanun/bcp@
|
|
299
|
+
npm install bcp@npm:@chidchanun/bcp@latest react react-dom
|
|
285
300
|
```
|
|
286
301
|
|
|
287
302
|
Application code then continues using:
|
|
@@ -292,7 +307,7 @@ import {
|
|
|
292
307
|
} from "bcp";
|
|
293
308
|
```
|
|
294
309
|
|
|
295
|
-
The scoped package can also be installed directly as `@chidchanun/bcp@
|
|
310
|
+
The scoped package can also be installed directly as `@chidchanun/bcp@latest`, but applications using the framework's documented `bcp` import path should prefer the alias form above.
|
|
296
311
|
|
|
297
312
|
## 12. Trusted publishing
|
|
298
313
|
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Protected Route Guards
|
|
2
|
+
|
|
3
|
+
BCP route guards provide server-side protection for pages and page subtrees without repeating authentication checks in every `loader.ts`.
|
|
4
|
+
|
|
5
|
+
A guard lives in `guard.ts` or `guard.tsx` inside the `app/` tree. It protects pages in that directory and descendant route directories.
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
app/
|
|
9
|
+
└─ dashboard/
|
|
10
|
+
├─ guard.ts
|
|
11
|
+
├─ page.tsx
|
|
12
|
+
└─ users/
|
|
13
|
+
└─ [id]/
|
|
14
|
+
├─ loader.ts
|
|
15
|
+
└─ page.tsx
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
## Authentication guard
|
|
19
|
+
|
|
20
|
+
```ts
|
|
21
|
+
// app/dashboard/guard.ts
|
|
22
|
+
import {
|
|
23
|
+
getSession,
|
|
24
|
+
redirect,
|
|
25
|
+
} from "bcp/server";
|
|
26
|
+
|
|
27
|
+
export async function guard() {
|
|
28
|
+
const session =
|
|
29
|
+
await getSession<{
|
|
30
|
+
userId: number;
|
|
31
|
+
email: string;
|
|
32
|
+
role: string;
|
|
33
|
+
}>();
|
|
34
|
+
|
|
35
|
+
if (!session) {
|
|
36
|
+
return redirect(
|
|
37
|
+
"/login",
|
|
38
|
+
303
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
session,
|
|
44
|
+
role:
|
|
45
|
+
session.role,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
The guard runs before the route loader and page render. Returning a Web `Response`, including `redirect()`, stops the pipeline immediately.
|
|
51
|
+
|
|
52
|
+
## Guard context
|
|
53
|
+
|
|
54
|
+
A guard receives:
|
|
55
|
+
|
|
56
|
+
```ts
|
|
57
|
+
interface GuardContext {
|
|
58
|
+
params: Record<string, string | string[] | undefined>;
|
|
59
|
+
searchParams: URLSearchParams;
|
|
60
|
+
parentData: Readonly<Record<string, unknown>>;
|
|
61
|
+
}
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`params` contains the matched route params and `searchParams` represents the target page URL. `parentData` contains the JSON-safe values returned by ancestor guards.
|
|
65
|
+
|
|
66
|
+
Example nested role guard:
|
|
67
|
+
|
|
68
|
+
```ts
|
|
69
|
+
// app/dashboard/admin/guard.ts
|
|
70
|
+
import {
|
|
71
|
+
redirect,
|
|
72
|
+
} from "bcp/server";
|
|
73
|
+
|
|
74
|
+
export async function guard({
|
|
75
|
+
parentData,
|
|
76
|
+
}) {
|
|
77
|
+
if (
|
|
78
|
+
parentData.role !==
|
|
79
|
+
"admin"
|
|
80
|
+
) {
|
|
81
|
+
return redirect(
|
|
82
|
+
"/dashboard",
|
|
83
|
+
303
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return {
|
|
88
|
+
section:
|
|
89
|
+
"admin",
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Guards execute from the app root toward the page directory. Later guards receive the merged output of earlier guards.
|
|
95
|
+
|
|
96
|
+
## Using guard data in a loader
|
|
97
|
+
|
|
98
|
+
Loaders receive the final merged guard data as `guardData`:
|
|
99
|
+
|
|
100
|
+
```ts
|
|
101
|
+
// app/dashboard/users/[id]/loader.ts
|
|
102
|
+
export async function loader({
|
|
103
|
+
params,
|
|
104
|
+
guardData,
|
|
105
|
+
}) {
|
|
106
|
+
return {
|
|
107
|
+
id:
|
|
108
|
+
params.id,
|
|
109
|
+
currentUser:
|
|
110
|
+
guardData.session,
|
|
111
|
+
role:
|
|
112
|
+
guardData.role,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
This avoids calling `getSession()` again after the guard has already validated the request.
|
|
118
|
+
|
|
119
|
+
## Using guard data in a page
|
|
120
|
+
|
|
121
|
+
Pages can read the same serialized guard result with `useGuardData<T>()`:
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
"use client";
|
|
125
|
+
|
|
126
|
+
import {
|
|
127
|
+
useGuardData,
|
|
128
|
+
} from "bcp";
|
|
129
|
+
|
|
130
|
+
export default function DashboardPage() {
|
|
131
|
+
const guard =
|
|
132
|
+
useGuardData<{
|
|
133
|
+
session: {
|
|
134
|
+
userId: number;
|
|
135
|
+
email: string;
|
|
136
|
+
};
|
|
137
|
+
role: string;
|
|
138
|
+
}>();
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<main>
|
|
142
|
+
Signed in as {
|
|
143
|
+
guard.session.email
|
|
144
|
+
}
|
|
145
|
+
</main>
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Guard data is available during SSR, hydration and SPA navigation. A guarded page does not need a `loader.ts` just to expose guard data.
|
|
151
|
+
|
|
152
|
+
## Serialization rules
|
|
153
|
+
|
|
154
|
+
Guard return values must be JSON-safe because they are passed to loaders and, when the page reads them, serialized into framework navigation/SSR data.
|
|
155
|
+
|
|
156
|
+
Supported values include:
|
|
157
|
+
|
|
158
|
+
- `null`
|
|
159
|
+
- strings
|
|
160
|
+
- booleans
|
|
161
|
+
- finite numbers
|
|
162
|
+
- arrays containing supported values
|
|
163
|
+
- plain objects containing supported values
|
|
164
|
+
|
|
165
|
+
Unsupported values include functions, symbols, BigInt, non-finite numbers, class instances, Date/Map/Set objects and circular references.
|
|
166
|
+
|
|
167
|
+
Do not place passwords, raw authentication secrets, private keys or other sensitive server-only secrets in guard data. Guard data may be serialized to the browser when the page uses the route framework data pipeline.
|
|
168
|
+
|
|
169
|
+
## Cookies and sessions
|
|
170
|
+
|
|
171
|
+
Guards run inside the normal BCP request context, so they can use:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import {
|
|
175
|
+
cookies,
|
|
176
|
+
getSession,
|
|
177
|
+
headers,
|
|
178
|
+
requestId,
|
|
179
|
+
requestMethod,
|
|
180
|
+
requestUrl,
|
|
181
|
+
} from "bcp/server";
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
Cookies set while an allowed guard runs are preserved on the final document or SPA navigation response. Cookies set before a guard redirect are preserved on that redirect as well.
|
|
185
|
+
|
|
186
|
+
## Direct requests and SPA navigation
|
|
187
|
+
|
|
188
|
+
The same guard rules apply to:
|
|
189
|
+
|
|
190
|
+
- direct browser requests
|
|
191
|
+
- `<Link>` navigation
|
|
192
|
+
- `navigate()`
|
|
193
|
+
- `router.push()`
|
|
194
|
+
- `router.replace()`
|
|
195
|
+
- browser history navigation
|
|
196
|
+
- loader-backed SPA navigation
|
|
197
|
+
|
|
198
|
+
For SPA navigation, the guard evaluates the target page URL rather than the internal `/_bcp/navigation` transport URL.
|
|
199
|
+
|
|
200
|
+
A guard redirect is converted to a navigation redirect payload when appropriate so same-origin navigation can continue through the BCP router while preserving response cookies.
|
|
201
|
+
|
|
202
|
+
## Middleware and guard order
|
|
203
|
+
|
|
204
|
+
Production request order is intentionally layered:
|
|
205
|
+
|
|
206
|
+
```text
|
|
207
|
+
Security gateway
|
|
208
|
+
↓
|
|
209
|
+
Response cache gateway
|
|
210
|
+
↓
|
|
211
|
+
Project middleware
|
|
212
|
+
↓
|
|
213
|
+
Route guard
|
|
214
|
+
↓
|
|
215
|
+
Page loader
|
|
216
|
+
↓
|
|
217
|
+
SSR / navigation payload
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
Project middleware therefore remains the outer application interception layer. Route guards are intended for page authorization and route-specific access policy.
|
|
221
|
+
|
|
222
|
+
## Cache safety
|
|
223
|
+
|
|
224
|
+
Pages protected by route guards are excluded from the automatic production page response cache. Guard results may depend on sessions, cookies, roles or user-specific state, so caching a guarded document without a user-aware key would be unsafe.
|
|
225
|
+
|
|
226
|
+
Applications can still use explicit server data caching where the cache key and invalidation strategy are safe for the data being cached.
|
|
227
|
+
|
|
228
|
+
## Internal transport hardening
|
|
229
|
+
|
|
230
|
+
Standalone production transports evaluated guard data to the inner page runtime through a private framework header. Incoming user-supplied values for that header are removed before guard evaluation and before proxying so a browser cannot forge a successful guard result.
|
|
231
|
+
|
|
232
|
+
The serialized internal guard payload is size-limited. Applications should return only the small identity/authorization data required by descendant guards, loaders and pages.
|
|
233
|
+
|
|
234
|
+
## Guard versus middleware
|
|
235
|
+
|
|
236
|
+
Use a route guard when access policy belongs to a page subtree and should integrate directly with loaders and page data.
|
|
237
|
+
|
|
238
|
+
Use middleware when a request-wide concern needs to run before route execution, such as rewrites, global redirects, shared request policy or non-page request interception.
|
|
239
|
+
|
|
240
|
+
Both can be used together; middleware runs before route guards.
|