@chidchanun/bcp 0.1.6 → 0.1.7
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 +31 -1
- package/README.md +77 -1
- package/docs/server-request-apis.md +404 -0
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +8 -6
- package/packages/bundler/src/server-production.ts +17 -0
- package/packages/client/src/server.ts +23 -0
- package/packages/server/src/index.ts +32 -13
- package/packages/server/src/request-context.ts +1043 -0
- package/packages/server/src/server-response.ts +82 -0
- package/packages/server/src/standalone-production-runtime-v2.ts +44 -13
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
All notable framework changes are tracked here before release.
|
|
4
4
|
|
|
5
|
+
## 0.1.7 - Server request context and cookies
|
|
6
|
+
|
|
7
|
+
### Server APIs
|
|
8
|
+
|
|
9
|
+
- Added the server-only `bcp/server` entrypoint for request-scoped helpers.
|
|
10
|
+
- Added `headers()` for reading an isolated copy of the active request headers.
|
|
11
|
+
- Added `requestUrl()` for reading an isolated copy of the active request URL, including pathname and search parameters.
|
|
12
|
+
- Added `requestMethod()` for reading the normalized HTTP method of the active request.
|
|
13
|
+
- Added `requestId()` with incoming `X-Request-Id` reuse and stable per-request UUID generation when no valid ID is supplied.
|
|
14
|
+
- Added `bearerToken()` for extracting Bearer credentials without imposing a JWT implementation.
|
|
15
|
+
- Added `clientIp()` with direct socket addresses by default and explicit `trustProxy` support for `Forwarded`, `X-Forwarded-For` and `X-Real-IP`.
|
|
16
|
+
- Forwarded client IP values are accepted only when they validate as IPv4 or IPv6 addresses, and IPv4-mapped IPv6 addresses are normalized.
|
|
17
|
+
- Added `json()` as a typed convenience wrapper around the standard Web `Response.json()` API.
|
|
18
|
+
- Added `redirect()` with relative URL resolution, HTTP/HTTPS validation and support for 301, 302, 303, 307 and 308 responses; the default redirect status is 307.
|
|
19
|
+
- Added `cookies()` with request-cookie `get`, `getAll` and `has` helpers.
|
|
20
|
+
- Added response-cookie `set` and `delete` support with domain, path, expires, max-age, HttpOnly, Secure and SameSite attributes.
|
|
21
|
+
- Response cookies created through `cookies()` are merged with cookies already present on the returned `Response` and remain separate `Set-Cookie` header values, including redirect responses.
|
|
22
|
+
- Request state is isolated with Node.js `AsyncLocalStorage` so concurrent requests do not share URL, method, request ID, header or cookie data.
|
|
23
|
+
- `bcp/server` is blocked from page/client graphs by the application boundary validator and is currently intended for API route handlers.
|
|
24
|
+
|
|
25
|
+
### Reliability
|
|
26
|
+
|
|
27
|
+
- Added request-context unit tests for URL/header access, cookie parsing, concurrent request isolation, response cookie serialization, deletion and validation.
|
|
28
|
+
- Added auth-request unit coverage for HTTP methods, incoming/generated request IDs, Bearer parsing, direct IP addresses, trusted proxy precedence and invalid forwarded-IP rejection.
|
|
29
|
+
- Added response-helper unit coverage for JSON responses, relative and absolute redirects, redirect status validation and protocol validation.
|
|
30
|
+
- Added client-boundary coverage for the new `bcp/server` entrypoint.
|
|
31
|
+
- Added development integration coverage for request URL/header helpers, auth request helpers, direct/proxy client IPs, JSON responses, redirects and response cookies through the full dev gateway stack.
|
|
32
|
+
- Added standalone production E2E coverage to verify `bcp/server` resolution, auth request helpers, direct/proxy client IPs, helper responses and multiple `Set-Cookie` values after production bundling.
|
|
33
|
+
- Package smoke checks verify that the `bcp/server` entrypoint and request-context runtime are included in publish artifacts.
|
|
34
|
+
|
|
5
35
|
## 0.1.6 - Critical CSS and release visibility
|
|
6
36
|
|
|
7
37
|
### Performance
|
|
@@ -36,7 +66,7 @@ All notable framework changes are tracked here before release.
|
|
|
36
66
|
|
|
37
67
|
- Tailwind projects now install the official `@tailwindcss/cli` package and compile CSS before starting BCP.
|
|
38
68
|
- Development runs Tailwind in watch mode alongside `bcp dev` using `concurrently`.
|
|
39
|
-
- Production builds minify Tailwind CSS before
|
|
69
|
+
- Production builds minify Tailwind CSS before the BCP production build.
|
|
40
70
|
- Generated Tailwind CSS is served through a framework API stylesheet endpoint with cache disabled during development.
|
|
41
71
|
- Root layout automatically links the generated stylesheet.
|
|
42
72
|
- Tailwind source detection is explicitly rooted at the generated project so utility classes in `app/` and other project files are discovered.
|
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
BCP Framework is a React full-stack framework with file-based routing, SSR, client navigation, API routes, middleware, metadata, client islands, cache/revalidation, security defaults and standalone production builds.
|
|
4
4
|
|
|
5
|
-
> Current development version: `0.1.
|
|
5
|
+
> Current development version: `0.1.7`. BCP is still pre-1.0 and validates each release candidate before the manual npm publish step.
|
|
6
6
|
|
|
7
7
|
## Quick start
|
|
8
8
|
|
|
@@ -121,6 +121,81 @@ export function GET() {
|
|
|
121
121
|
|
|
122
122
|
Supported methods include GET, POST, PUT, PATCH, DELETE, HEAD and OPTIONS. HEAD falls back to GET when no explicit HEAD handler exists, and OPTIONS is generated automatically when appropriate.
|
|
123
123
|
|
|
124
|
+
## Server request APIs
|
|
125
|
+
|
|
126
|
+
BCP 0.1.7 adds request-scoped server helpers through `bcp/server`. In the current 0.1.x client pipeline these helpers are intended for API routes and are rejected from page/client module graphs.
|
|
127
|
+
|
|
128
|
+
```ts
|
|
129
|
+
import {
|
|
130
|
+
bearerToken,
|
|
131
|
+
clientIp,
|
|
132
|
+
cookies,
|
|
133
|
+
json,
|
|
134
|
+
requestId,
|
|
135
|
+
requestMethod,
|
|
136
|
+
requestUrl,
|
|
137
|
+
} from "bcp/server";
|
|
138
|
+
|
|
139
|
+
export async function GET() {
|
|
140
|
+
const url =
|
|
141
|
+
await requestUrl();
|
|
142
|
+
|
|
143
|
+
return json({
|
|
144
|
+
pathname:
|
|
145
|
+
url.pathname,
|
|
146
|
+
method:
|
|
147
|
+
await requestMethod(),
|
|
148
|
+
requestId:
|
|
149
|
+
await requestId(),
|
|
150
|
+
bearerToken:
|
|
151
|
+
await bearerToken(),
|
|
152
|
+
clientIp:
|
|
153
|
+
await clientIp(),
|
|
154
|
+
session:
|
|
155
|
+
(
|
|
156
|
+
await cookies()
|
|
157
|
+
).get(
|
|
158
|
+
"session"
|
|
159
|
+
)?.value ?? null,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
`requestId()` reuses a valid incoming `X-Request-Id` or generates one stable UUID for the request. `bearerToken()` extracts a Bearer credential without decoding or verifying it. `clientIp()` uses the direct socket address by default; applications behind a trusted reverse proxy can explicitly use `clientIp({ trustProxy: true })` to read validated `Forwarded`, `X-Forwarded-For` or `X-Real-IP` values.
|
|
165
|
+
|
|
166
|
+
Response cookies and redirects can be composed for login-style flows:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
import {
|
|
170
|
+
cookies,
|
|
171
|
+
redirect,
|
|
172
|
+
} from "bcp/server";
|
|
173
|
+
|
|
174
|
+
export async function POST() {
|
|
175
|
+
const cookieStore =
|
|
176
|
+
await cookies();
|
|
177
|
+
|
|
178
|
+
cookieStore.set(
|
|
179
|
+
"session",
|
|
180
|
+
"session-token",
|
|
181
|
+
{
|
|
182
|
+
httpOnly: true,
|
|
183
|
+
secure: true,
|
|
184
|
+
sameSite: "lax",
|
|
185
|
+
path: "/",
|
|
186
|
+
maxAge: 60 * 60 * 12,
|
|
187
|
+
}
|
|
188
|
+
);
|
|
189
|
+
|
|
190
|
+
return redirect(
|
|
191
|
+
"/dashboard",
|
|
192
|
+
303
|
|
193
|
+
);
|
|
194
|
+
}
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`redirect()` defaults to status `307`; supported statuses are `301`, `302`, `303`, `307` and `308`. Use `cookieStore.delete("session")` to expire a cookie. See [Server Request APIs](docs/server-request-apis.md) for proxy trust, URL handling, response helpers, request isolation, cookie options and boundary details.
|
|
198
|
+
|
|
124
199
|
## Metadata
|
|
125
200
|
|
|
126
201
|
```ts
|
|
@@ -262,6 +337,7 @@ No real npm publish command is run automatically by the repository.
|
|
|
262
337
|
|
|
263
338
|
- [Getting Started](docs/getting-started.md)
|
|
264
339
|
- [Application Modules](docs/application-modules.md)
|
|
340
|
+
- [Server Request APIs](docs/server-request-apis.md)
|
|
265
341
|
- [Routing](docs/routing.md)
|
|
266
342
|
- [Configuration](docs/configuration.md)
|
|
267
343
|
- [Caching](docs/caching.md)
|
|
@@ -0,0 +1,404 @@
|
|
|
1
|
+
# Server Request APIs
|
|
2
|
+
|
|
3
|
+
BCP Framework 0.1.7 introduces request-scoped server helpers through `bcp/server`.
|
|
4
|
+
|
|
5
|
+
These APIs are server-only and are intended for API route handlers in the current 0.1.x client pipeline. Importing `bcp/server` from a page, layout, client island or another module reachable from the browser bundle is rejected by the client-boundary validator.
|
|
6
|
+
|
|
7
|
+
## Headers
|
|
8
|
+
|
|
9
|
+
Use `headers()` to read a copy of the current request headers:
|
|
10
|
+
|
|
11
|
+
```ts
|
|
12
|
+
import {
|
|
13
|
+
headers,
|
|
14
|
+
} from "bcp/server";
|
|
15
|
+
|
|
16
|
+
export async function GET() {
|
|
17
|
+
const requestHeaders =
|
|
18
|
+
await headers();
|
|
19
|
+
|
|
20
|
+
return Response.json({
|
|
21
|
+
userAgent:
|
|
22
|
+
requestHeaders.get(
|
|
23
|
+
"user-agent"
|
|
24
|
+
),
|
|
25
|
+
requestId:
|
|
26
|
+
requestHeaders.get(
|
|
27
|
+
"x-request-id"
|
|
28
|
+
),
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
The returned `Headers` object is a copy. Mutating it does not modify the original incoming request.
|
|
34
|
+
|
|
35
|
+
## Request URL
|
|
36
|
+
|
|
37
|
+
Use `requestUrl()` to read the complete URL of the current request, including pathname and search parameters:
|
|
38
|
+
|
|
39
|
+
```ts
|
|
40
|
+
import {
|
|
41
|
+
json,
|
|
42
|
+
requestUrl,
|
|
43
|
+
} from "bcp/server";
|
|
44
|
+
|
|
45
|
+
export async function GET() {
|
|
46
|
+
const url =
|
|
47
|
+
await requestUrl();
|
|
48
|
+
|
|
49
|
+
return json({
|
|
50
|
+
pathname:
|
|
51
|
+
url.pathname,
|
|
52
|
+
query:
|
|
53
|
+
url.searchParams.get(
|
|
54
|
+
"query"
|
|
55
|
+
),
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
The returned `URL` is a copy. Mutating it does not change the request context seen by another helper call.
|
|
61
|
+
|
|
62
|
+
## Request method
|
|
63
|
+
|
|
64
|
+
`requestMethod()` returns the normalized uppercase HTTP method for the active request:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
import {
|
|
68
|
+
requestMethod,
|
|
69
|
+
} from "bcp/server";
|
|
70
|
+
|
|
71
|
+
export async function POST() {
|
|
72
|
+
return Response.json({
|
|
73
|
+
method:
|
|
74
|
+
await requestMethod(),
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
A POST request returns `"POST"`, a GET request returns `"GET"`, and so on.
|
|
80
|
+
|
|
81
|
+
## Request ID
|
|
82
|
+
|
|
83
|
+
`requestId()` provides one stable identifier for the active request:
|
|
84
|
+
|
|
85
|
+
```ts
|
|
86
|
+
import {
|
|
87
|
+
requestId,
|
|
88
|
+
} from "bcp/server";
|
|
89
|
+
|
|
90
|
+
export async function GET() {
|
|
91
|
+
const id =
|
|
92
|
+
await requestId();
|
|
93
|
+
|
|
94
|
+
return Response.json({
|
|
95
|
+
requestId:
|
|
96
|
+
id,
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
When the incoming request includes a valid `X-Request-Id`, BCP preserves that value. If the header is missing, blank, longer than 256 characters or contains control characters, BCP generates a UUID once and every `requestId()` call in that request returns the same generated value.
|
|
102
|
+
|
|
103
|
+
## Bearer tokens
|
|
104
|
+
|
|
105
|
+
Use `bearerToken()` to extract an HTTP Bearer credential from the `Authorization` header:
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import {
|
|
109
|
+
bearerToken,
|
|
110
|
+
} from "bcp/server";
|
|
111
|
+
|
|
112
|
+
export async function GET() {
|
|
113
|
+
const token =
|
|
114
|
+
await bearerToken();
|
|
115
|
+
|
|
116
|
+
if (!token) {
|
|
117
|
+
return Response.json(
|
|
118
|
+
{
|
|
119
|
+
error:
|
|
120
|
+
"Unauthorized",
|
|
121
|
+
},
|
|
122
|
+
{
|
|
123
|
+
status:
|
|
124
|
+
401,
|
|
125
|
+
}
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return Response.json({
|
|
130
|
+
authenticated:
|
|
131
|
+
true,
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
The `Bearer` scheme is matched case-insensitively. BCP returns the credential string after the scheme and does not decode, verify or assume JWT. Missing authorization and non-Bearer schemes return `null`.
|
|
137
|
+
|
|
138
|
+
## Client IP
|
|
139
|
+
|
|
140
|
+
`clientIp()` reads the direct connection address attached to the BCP request context:
|
|
141
|
+
|
|
142
|
+
```ts
|
|
143
|
+
import {
|
|
144
|
+
clientIp,
|
|
145
|
+
} from "bcp/server";
|
|
146
|
+
|
|
147
|
+
export async function GET() {
|
|
148
|
+
return Response.json({
|
|
149
|
+
ip:
|
|
150
|
+
await clientIp(),
|
|
151
|
+
});
|
|
152
|
+
}
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
Proxy forwarding headers are not trusted by default. Applications behind a trusted reverse proxy can opt in explicitly:
|
|
156
|
+
|
|
157
|
+
```ts
|
|
158
|
+
const ip =
|
|
159
|
+
await clientIp({
|
|
160
|
+
trustProxy:
|
|
161
|
+
true,
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
When `trustProxy` is enabled, BCP checks client-address sources in this order:
|
|
166
|
+
|
|
167
|
+
1. the standardized `Forwarded` header (`for=`)
|
|
168
|
+
2. the first usable value in `X-Forwarded-For`
|
|
169
|
+
3. `X-Real-IP`
|
|
170
|
+
4. the direct connection address
|
|
171
|
+
|
|
172
|
+
Do not enable `trustProxy` when clients can connect directly without a trusted proxy stripping or replacing forwarding headers, because forwarded client-IP headers can otherwise be spoofed.
|
|
173
|
+
|
|
174
|
+
IPv4-mapped IPv6 addresses such as `::ffff:127.0.0.1` are normalized to `127.0.0.1`.
|
|
175
|
+
|
|
176
|
+
## JSON responses
|
|
177
|
+
|
|
178
|
+
`json()` is a small wrapper around the standard Web `Response.json()` API. It accepts the response body and an optional `ResponseInit`:
|
|
179
|
+
|
|
180
|
+
```ts
|
|
181
|
+
import {
|
|
182
|
+
json,
|
|
183
|
+
} from "bcp/server";
|
|
184
|
+
|
|
185
|
+
export function POST() {
|
|
186
|
+
return json(
|
|
187
|
+
{
|
|
188
|
+
success: true,
|
|
189
|
+
},
|
|
190
|
+
{
|
|
191
|
+
status: 201,
|
|
192
|
+
headers: {
|
|
193
|
+
"x-created": "yes",
|
|
194
|
+
},
|
|
195
|
+
}
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
BCP keeps the Web Response model, so existing `Response` objects and `Response.json()` continue to work alongside the helper.
|
|
201
|
+
|
|
202
|
+
## Redirect responses
|
|
203
|
+
|
|
204
|
+
Use `redirect()` to create HTTP redirects. Relative destinations are resolved against the current request URL:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import {
|
|
208
|
+
redirect,
|
|
209
|
+
} from "bcp/server";
|
|
210
|
+
|
|
211
|
+
export function POST() {
|
|
212
|
+
return redirect(
|
|
213
|
+
"/dashboard",
|
|
214
|
+
303
|
|
215
|
+
);
|
|
216
|
+
}
|
|
217
|
+
```
|
|
218
|
+
|
|
219
|
+
The default status is `307`. Supported redirect statuses are `301`, `302`, `303`, `307` and `308`. Redirect destinations must use HTTP or HTTPS. Absolute HTTP/HTTPS URLs are also supported.
|
|
220
|
+
|
|
221
|
+
A common login-style flow can combine response cookies and redirects:
|
|
222
|
+
|
|
223
|
+
```ts
|
|
224
|
+
import {
|
|
225
|
+
cookies,
|
|
226
|
+
redirect,
|
|
227
|
+
} from "bcp/server";
|
|
228
|
+
|
|
229
|
+
export async function POST() {
|
|
230
|
+
const cookieStore =
|
|
231
|
+
await cookies();
|
|
232
|
+
|
|
233
|
+
cookieStore.set(
|
|
234
|
+
"session",
|
|
235
|
+
"session-token",
|
|
236
|
+
{
|
|
237
|
+
httpOnly: true,
|
|
238
|
+
secure: true,
|
|
239
|
+
sameSite: "lax",
|
|
240
|
+
path: "/",
|
|
241
|
+
}
|
|
242
|
+
);
|
|
243
|
+
|
|
244
|
+
return redirect(
|
|
245
|
+
"/dashboard",
|
|
246
|
+
303
|
|
247
|
+
);
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
BCP applies pending response cookies after the handler resolves, so cookies created before `redirect()` are preserved on the redirect response.
|
|
252
|
+
|
|
253
|
+
## Request cookies
|
|
254
|
+
|
|
255
|
+
Use `cookies()` to inspect cookies sent by the client:
|
|
256
|
+
|
|
257
|
+
```ts
|
|
258
|
+
import {
|
|
259
|
+
cookies,
|
|
260
|
+
} from "bcp/server";
|
|
261
|
+
|
|
262
|
+
export async function GET() {
|
|
263
|
+
const cookieStore =
|
|
264
|
+
await cookies();
|
|
265
|
+
|
|
266
|
+
return Response.json({
|
|
267
|
+
session:
|
|
268
|
+
cookieStore.get(
|
|
269
|
+
"session"
|
|
270
|
+
)?.value ?? null,
|
|
271
|
+
hasTheme:
|
|
272
|
+
cookieStore.has(
|
|
273
|
+
"theme"
|
|
274
|
+
),
|
|
275
|
+
all:
|
|
276
|
+
cookieStore.getAll(),
|
|
277
|
+
});
|
|
278
|
+
}
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
Request cookie values are percent-decoded when possible. Invalid percent encoding is preserved instead of failing the request.
|
|
282
|
+
|
|
283
|
+
## Setting response cookies
|
|
284
|
+
|
|
285
|
+
`cookies().set()` records a `Set-Cookie` value that BCP applies to the API response:
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
import {
|
|
289
|
+
cookies,
|
|
290
|
+
} from "bcp/server";
|
|
291
|
+
|
|
292
|
+
export async function POST() {
|
|
293
|
+
const cookieStore =
|
|
294
|
+
await cookies();
|
|
295
|
+
|
|
296
|
+
cookieStore.set(
|
|
297
|
+
"session",
|
|
298
|
+
"session-token",
|
|
299
|
+
{
|
|
300
|
+
httpOnly: true,
|
|
301
|
+
secure: true,
|
|
302
|
+
sameSite: "lax",
|
|
303
|
+
path: "/",
|
|
304
|
+
maxAge: 60 * 60 * 12,
|
|
305
|
+
}
|
|
306
|
+
);
|
|
307
|
+
|
|
308
|
+
return Response.json({
|
|
309
|
+
success: true,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
```
|
|
313
|
+
|
|
314
|
+
The object form is also supported:
|
|
315
|
+
|
|
316
|
+
```ts
|
|
317
|
+
cookieStore.set({
|
|
318
|
+
name: "refreshToken",
|
|
319
|
+
value: "token-value",
|
|
320
|
+
httpOnly: true,
|
|
321
|
+
secure: true,
|
|
322
|
+
sameSite: "strict",
|
|
323
|
+
path: "/api",
|
|
324
|
+
expires:
|
|
325
|
+
new Date(
|
|
326
|
+
Date.now() +
|
|
327
|
+
7 * 24 * 60 * 60 * 1000
|
|
328
|
+
),
|
|
329
|
+
});
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Supported response-cookie options are:
|
|
333
|
+
|
|
334
|
+
- `domain`
|
|
335
|
+
- `path`
|
|
336
|
+
- `expires`
|
|
337
|
+
- `maxAge`
|
|
338
|
+
- `httpOnly`
|
|
339
|
+
- `secure`
|
|
340
|
+
- `sameSite`: `"lax"`, `"strict"` or `"none"`
|
|
341
|
+
|
|
342
|
+
Multiple response cookies remain separate `Set-Cookie` header values. Cookies already present on the returned `Response` are preserved.
|
|
343
|
+
|
|
344
|
+
## Deleting cookies
|
|
345
|
+
|
|
346
|
+
Deleting a cookie emits an expired cookie with `Max-Age=0`:
|
|
347
|
+
|
|
348
|
+
```ts
|
|
349
|
+
export async function DELETE() {
|
|
350
|
+
const cookieStore =
|
|
351
|
+
await cookies();
|
|
352
|
+
|
|
353
|
+
cookieStore.delete(
|
|
354
|
+
"session"
|
|
355
|
+
);
|
|
356
|
+
|
|
357
|
+
return Response.json({
|
|
358
|
+
success: true,
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
When the original cookie uses a custom path or domain, supply the same values when deleting it:
|
|
364
|
+
|
|
365
|
+
```ts
|
|
366
|
+
cookieStore.delete(
|
|
367
|
+
"session",
|
|
368
|
+
{
|
|
369
|
+
path: "/admin",
|
|
370
|
+
domain: "example.com",
|
|
371
|
+
}
|
|
372
|
+
);
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
## Request isolation
|
|
376
|
+
|
|
377
|
+
BCP stores request state with Node.js `AsyncLocalStorage`. Concurrent requests receive independent URL, method, request ID, headers and cookies, so request data from one user is not shared with another request.
|
|
378
|
+
|
|
379
|
+
Calling a request-scoped helper without an active BCP request context throws an error. A relative `redirect()` also requires the active request context because it resolves the destination against the current URL.
|
|
380
|
+
|
|
381
|
+
## Client boundary
|
|
382
|
+
|
|
383
|
+
This is invalid in the current 0.1.x page/client pipeline:
|
|
384
|
+
|
|
385
|
+
```tsx
|
|
386
|
+
import {
|
|
387
|
+
cookies,
|
|
388
|
+
} from "bcp/server";
|
|
389
|
+
|
|
390
|
+
export default function Page() {
|
|
391
|
+
return null;
|
|
392
|
+
}
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
Move the server operation behind an API route instead:
|
|
396
|
+
|
|
397
|
+
```text
|
|
398
|
+
page/client
|
|
399
|
+
-> fetch("/api/session")
|
|
400
|
+
-> API route
|
|
401
|
+
-> bcp/server
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Future server-rendered data primitives can extend these helpers to page-level server execution without exposing them to the browser bundle.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@chidchanun/bcp",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.7",
|
|
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",
|
|
@@ -43,6 +43,11 @@
|
|
|
43
43
|
"types": "./packages/client/src/config.ts",
|
|
44
44
|
"default": "./packages/client/src/config.ts"
|
|
45
45
|
},
|
|
46
|
+
"./server": {
|
|
47
|
+
"types": "./packages/client/src/server.ts",
|
|
48
|
+
"browser": "./packages/client/src/server-only.browser.mjs",
|
|
49
|
+
"default": "./packages/client/src/server.ts"
|
|
50
|
+
},
|
|
46
51
|
"./server-only": {
|
|
47
52
|
"types": "./packages/client/src/server-only.d.ts",
|
|
48
53
|
"browser": "./packages/client/src/server-only.browser.mjs",
|
|
@@ -22,6 +22,8 @@ const MODULE_EXTENSIONS = [
|
|
|
22
22
|
".cjs",
|
|
23
23
|
] as const;
|
|
24
24
|
|
|
25
|
+
const SERVER_ONLY_IMPORTS = new Set(["bcp/server","bcp/server-only"])
|
|
26
|
+
|
|
25
27
|
export function validateClientBoundaries(
|
|
26
28
|
routes: Route[],
|
|
27
29
|
rootDirectory: string
|
|
@@ -127,13 +129,13 @@ function visitClientModule(
|
|
|
127
129
|
source
|
|
128
130
|
)
|
|
129
131
|
) {
|
|
130
|
-
if (
|
|
131
|
-
specifier ===
|
|
132
|
-
"bcp/server-only"
|
|
133
|
-
) {
|
|
132
|
+
if (SERVER_ONLY_IMPORTS.has(specifier)){
|
|
134
133
|
throw new Error(
|
|
135
|
-
`BCP Framework: ${formatApplicationPath(
|
|
136
|
-
|
|
134
|
+
`BCP Framework: ${formatApplicationPath(
|
|
135
|
+
rootDirectory,
|
|
136
|
+
filePath
|
|
137
|
+
)} imports ${specifier} but is reachable from the client bundle for ${routeName}. Move the server dependency behind an API route.`
|
|
138
|
+
)
|
|
137
139
|
}
|
|
138
140
|
|
|
139
141
|
const dependency =
|
|
@@ -172,6 +172,12 @@ export async function buildProductionServer(
|
|
|
172
172
|
"../../client/src/islands.tsx"
|
|
173
173
|
);
|
|
174
174
|
|
|
175
|
+
const frameworkServerEntry =
|
|
176
|
+
path.resolve(
|
|
177
|
+
frameworkDirectory,
|
|
178
|
+
"../../client/src/server.ts"
|
|
179
|
+
);
|
|
180
|
+
|
|
175
181
|
await build({
|
|
176
182
|
absWorkingDir:
|
|
177
183
|
rootDirectory,
|
|
@@ -223,6 +229,17 @@ export async function buildProductionServer(
|
|
|
223
229
|
frameworkIslandEntry,
|
|
224
230
|
})
|
|
225
231
|
);
|
|
232
|
+
|
|
233
|
+
buildApi.onResolve(
|
|
234
|
+
{
|
|
235
|
+
filter:
|
|
236
|
+
/^bcp\/server$/,
|
|
237
|
+
},
|
|
238
|
+
() => ({
|
|
239
|
+
path:
|
|
240
|
+
frameworkServerEntry,
|
|
241
|
+
})
|
|
242
|
+
);
|
|
226
243
|
},
|
|
227
244
|
},
|
|
228
245
|
],
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
export {
|
|
2
|
+
bearerToken,
|
|
3
|
+
clientIp,
|
|
4
|
+
cookies,
|
|
5
|
+
headers,
|
|
6
|
+
requestId,
|
|
7
|
+
requestMethod,
|
|
8
|
+
requestUrl,
|
|
9
|
+
|
|
10
|
+
type ClientIpOptions,
|
|
11
|
+
type CookieSameSite,
|
|
12
|
+
type RequestCookie,
|
|
13
|
+
type RequestCookieStore,
|
|
14
|
+
type ResponseCookie,
|
|
15
|
+
type ResponseCookieOptions,
|
|
16
|
+
} from "../../server/src/request-context.js";
|
|
17
|
+
|
|
18
|
+
export {
|
|
19
|
+
json,
|
|
20
|
+
redirect,
|
|
21
|
+
|
|
22
|
+
type RedirectStatus,
|
|
23
|
+
} from "../../server/src/server-response.js";
|