@chidchanun/bcp 0.2.5 → 0.2.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/README.md +166 -67
- package/docs/README.md +43 -53
- package/docs/api-manifest.json +16 -3
- package/docs/api-reference.md +78 -6
- package/docs/auth-route-guards.md +58 -50
- package/docs/authorization-security.md +298 -0
- package/docs/development-logging.md +44 -1
- package/docs/docs-web-manifest.json +10 -6
- package/docs/observability.md +445 -0
- package/docs/platform-manifest.json +18 -4
- package/docs/releases/0.2.6.md +194 -0
- package/docs/releases/0.2.7.md +205 -0
- package/docs/security.md +55 -1
- package/package.json +6 -1
- package/packages/bundler/src/client-boundary.ts +1 -0
- package/packages/client/src/auth.ts +20 -0
- package/packages/client/src/observability.ts +22 -0
- package/packages/client/src/server.ts +16 -0
- package/packages/server/src/auth-guard.ts +111 -15
- package/packages/server/src/authorization.ts +368 -0
- package/packages/server/src/observability.ts +1258 -0
- package/packages/server/src/request-security.ts +596 -0
package/docs/api-reference.md
CHANGED
|
@@ -164,23 +164,33 @@ Related guides: [Database](database.md), [Database Migrations](database-migratio
|
|
|
164
164
|
|
|
165
165
|
## `bcp/auth`
|
|
166
166
|
|
|
167
|
-
Server-only
|
|
167
|
+
Server-only authentication and authorization APIs.
|
|
168
168
|
|
|
169
169
|
```ts
|
|
170
170
|
import {
|
|
171
|
+
AuthorizationError,
|
|
172
|
+
assertPermission,
|
|
171
173
|
auth,
|
|
174
|
+
authorize,
|
|
175
|
+
can,
|
|
176
|
+
cannot,
|
|
172
177
|
createAuth,
|
|
173
178
|
createAuthGuard,
|
|
174
179
|
createGuestGuard,
|
|
175
180
|
createMemoryAuthSessionStore,
|
|
181
|
+
createPermissionGuard,
|
|
176
182
|
createRoleGuard,
|
|
183
|
+
defineAuthorizationPolicy,
|
|
177
184
|
getGuardAuth,
|
|
178
185
|
getSession,
|
|
186
|
+
getUserPermissions,
|
|
187
|
+
hasPermission,
|
|
179
188
|
login,
|
|
180
189
|
logout,
|
|
181
190
|
logoutAll,
|
|
182
191
|
requireAuth,
|
|
183
192
|
requireGuest,
|
|
193
|
+
requirePermission,
|
|
184
194
|
requireRole,
|
|
185
195
|
revokeSession,
|
|
186
196
|
revokeUserSessions,
|
|
@@ -192,45 +202,107 @@ import {
|
|
|
192
202
|
type AuthSessionStore,
|
|
193
203
|
type AuthSessionStoreRecord,
|
|
194
204
|
type AuthUser,
|
|
205
|
+
type AuthorizationContext,
|
|
206
|
+
type AuthorizationMatch,
|
|
207
|
+
type AuthorizationPolicy,
|
|
195
208
|
type MemoryAuthSessionStore,
|
|
209
|
+
type PermissionCheckOptions,
|
|
210
|
+
type PermissionRequirement,
|
|
211
|
+
type RequirePermissionOptions,
|
|
196
212
|
} from "bcp/auth";
|
|
197
213
|
```
|
|
198
214
|
|
|
199
|
-
The default mode remains stateless signed JWT-cookie authentication.
|
|
215
|
+
The default authentication mode remains stateless signed JWT-cookie authentication.
|
|
200
216
|
|
|
201
217
|
Configure `AuthOptions.store` to enable server-side session revocation and `idleTimeout`. `createMemoryAuthSessionStore()` is provided for development/testing; production multi-instance applications should implement `AuthSessionStore` with shared durable storage.
|
|
202
218
|
|
|
203
|
-
|
|
219
|
+
Authorization & Security v2 adds permission checks, permission route guards and resource-aware policy functions:
|
|
204
220
|
|
|
205
|
-
|
|
221
|
+
- `hasPermission()` / `assertPermission()` for flat permission or scope fields,
|
|
222
|
+
- `requirePermission()` / `createPermissionGuard()` for route protection,
|
|
223
|
+
- `defineAuthorizationPolicy()` for application policy definitions,
|
|
224
|
+
- `can()` / `cannot()` for policy checks,
|
|
225
|
+
- `authorize()` for throwing `AuthorizationError` when a policy denies access.
|
|
226
|
+
|
|
227
|
+
Related guides: [Authentication](authentication.md), [Auth Session Stores](auth-session-store.md), [Auth Route Guards](auth-route-guards.md), [Authorization & Security v2](authorization-security.md), [JWT Sessions](session-auth.md).
|
|
228
|
+
|
|
229
|
+
## `bcp/observability`
|
|
230
|
+
|
|
231
|
+
Server-only Observability Platform v2 APIs.
|
|
232
|
+
|
|
233
|
+
```ts
|
|
234
|
+
import {
|
|
235
|
+
createHealthRegistry,
|
|
236
|
+
createMetricsRegistry,
|
|
237
|
+
createMetricsResponse,
|
|
238
|
+
createRequestMetricsMiddleware,
|
|
239
|
+
type CounterMetric,
|
|
240
|
+
type GaugeMetric,
|
|
241
|
+
type HealthCheck,
|
|
242
|
+
type HealthCheckOptions,
|
|
243
|
+
type HealthCheckReportItem,
|
|
244
|
+
type HealthCheckResult,
|
|
245
|
+
type HealthRegistry,
|
|
246
|
+
type HealthReport,
|
|
247
|
+
type HistogramMetric,
|
|
248
|
+
type HistogramOptions,
|
|
249
|
+
type MetricDefinitionOptions,
|
|
250
|
+
type MetricLabels,
|
|
251
|
+
type MetricLabelValue,
|
|
252
|
+
type MetricsRegistry,
|
|
253
|
+
type RequestMetricsOptions,
|
|
254
|
+
} from "bcp/observability";
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
`createMetricsRegistry()` provides process-local counters, gauges and histograms. `createMetricsResponse()` renders Prometheus-compatible text exposition. `createRequestMetricsMiddleware()` measures HTTP request counts and duration using bounded `method` / `status` labels by default and intentionally does not label by raw path.
|
|
258
|
+
|
|
259
|
+
`createHealthRegistry()` registers synchronous or asynchronous liveness/readiness checks, applies per-check timeouts and produces JSON health responses with HTTP `200` when all checks pass or `503` when any check fails.
|
|
260
|
+
|
|
261
|
+
The built-in registry is process-local. Multi-instance deployments should scrape each process/container or aggregate through external monitoring infrastructure.
|
|
262
|
+
|
|
263
|
+
Related guides: [Observability Platform v2](observability.md), [Logging](development-logging.md).
|
|
206
264
|
|
|
207
265
|
## `bcp/server`
|
|
208
266
|
|
|
209
267
|
Server request/runtime APIs.
|
|
210
268
|
|
|
211
|
-
This entrypoint includes request context, cookies, logging, graceful shutdown hooks, multipart upload helpers, storage adapters, file delivery, response helpers and low-level session primitives.
|
|
269
|
+
This entrypoint includes request context, cookies, CSRF and same-origin protection, logging, graceful shutdown hooks, multipart upload helpers, storage adapters, file delivery, response helpers and low-level session primitives.
|
|
212
270
|
|
|
213
271
|
```ts
|
|
214
272
|
import {
|
|
215
273
|
clientIp,
|
|
216
274
|
cookies,
|
|
275
|
+
createCsrfToken,
|
|
217
276
|
createLocalStorage,
|
|
218
277
|
createLogger,
|
|
219
278
|
createS3Storage,
|
|
220
279
|
createStorageResponse,
|
|
280
|
+
destroyCsrfToken,
|
|
221
281
|
getProductionHardeningConfig,
|
|
222
282
|
headers,
|
|
283
|
+
isSafeHttpMethod,
|
|
284
|
+
isSameOriginRequest,
|
|
223
285
|
json,
|
|
224
286
|
redirect,
|
|
225
287
|
registerShutdownHook,
|
|
226
288
|
requestId,
|
|
227
289
|
requestMethod,
|
|
228
290
|
requestUrl,
|
|
291
|
+
requireCsrfRequest,
|
|
292
|
+
requireSameOriginRequest,
|
|
229
293
|
storeMultipartFile,
|
|
294
|
+
verifyCsrfRequest,
|
|
295
|
+
verifyCsrfToken,
|
|
296
|
+
RequestSecurityError,
|
|
297
|
+
type CsrfTokenOptions,
|
|
298
|
+
type SameOriginOptions,
|
|
299
|
+
type VerifyCsrfRequestOptions,
|
|
230
300
|
} from "bcp/server";
|
|
231
301
|
```
|
|
232
302
|
|
|
233
|
-
|
|
303
|
+
`requireSameOriginRequest()` protects unsafe cookie-authenticated mutations by validating `Origin`/`Referer`. `createCsrfToken()` and `requireCsrfRequest()` provide signed double-submit style CSRF protection. `BCP_CSRF_SECRET` is preferred when configured and falls back to `BCP_SESSION_SECRET`.
|
|
304
|
+
|
|
305
|
+
Related guides: [Server Request APIs](server-request-apis.md), [Authorization & Security v2](authorization-security.md), [File Upload](file-upload.md), [Storage](storage.md), [Storage Ecosystem](storage-ecosystem.md), [Production Hardening](production-hardening.md).
|
|
234
306
|
|
|
235
307
|
## `bcp/server-only`
|
|
236
308
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Auth Route Guards
|
|
2
2
|
|
|
3
|
-
BCP Framework integrates Authentication Platform v2 with route guards through the server-only `bcp/auth` entrypoint.
|
|
3
|
+
BCP Framework integrates Authentication Platform v2 and Authorization & Security v2 with route guards through the server-only `bcp/auth` entrypoint.
|
|
4
4
|
|
|
5
5
|
## Protect a route tree
|
|
6
6
|
|
|
@@ -30,7 +30,7 @@ export const guard =
|
|
|
30
30
|
});
|
|
31
31
|
```
|
|
32
32
|
|
|
33
|
-
## Guest-only routes — 0.2.5
|
|
33
|
+
## Guest-only routes — 0.2.5+
|
|
34
34
|
|
|
35
35
|
Login, registration, and password-recovery entry pages often should not remain accessible after the user has already authenticated.
|
|
36
36
|
|
|
@@ -60,30 +60,9 @@ signed-in user
|
|
|
60
60
|
303 /dashboard
|
|
61
61
|
```
|
|
62
62
|
|
|
63
|
-
Use `redirectTo: null` to return `409 Already authenticated` instead of redirecting
|
|
63
|
+
Use `redirectTo: null` to return `409 Already authenticated` instead of redirecting.
|
|
64
64
|
|
|
65
|
-
|
|
66
|
-
export const guard =
|
|
67
|
-
createGuestGuard({
|
|
68
|
-
redirectTo:
|
|
69
|
-
null,
|
|
70
|
-
});
|
|
71
|
-
```
|
|
72
|
-
|
|
73
|
-
The low-level form is `requireGuest()`:
|
|
74
|
-
|
|
75
|
-
```ts
|
|
76
|
-
import {
|
|
77
|
-
requireGuest,
|
|
78
|
-
} from "bcp/auth";
|
|
79
|
-
|
|
80
|
-
export function guard() {
|
|
81
|
-
return requireGuest({
|
|
82
|
-
redirectTo:
|
|
83
|
-
"/dashboard",
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
```
|
|
65
|
+
The low-level form is `requireGuest()`.
|
|
87
66
|
|
|
88
67
|
## Require a role
|
|
89
68
|
|
|
@@ -110,58 +89,87 @@ export const guard =
|
|
|
110
89
|
|
|
111
90
|
An authenticated user without the required role receives `403 Forbidden` by default.
|
|
112
91
|
|
|
113
|
-
|
|
92
|
+
Require one of several roles:
|
|
114
93
|
|
|
115
94
|
```ts
|
|
116
95
|
export const guard =
|
|
117
|
-
createRoleGuard<User>(
|
|
96
|
+
createRoleGuard<User>([
|
|
118
97
|
"admin",
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
"/forbidden",
|
|
122
|
-
}
|
|
123
|
-
);
|
|
98
|
+
"manager",
|
|
99
|
+
]);
|
|
124
100
|
```
|
|
125
101
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
The default role matching mode is `any`:
|
|
102
|
+
Require all values from a role field:
|
|
129
103
|
|
|
130
104
|
```ts
|
|
131
105
|
export const guard =
|
|
132
|
-
createRoleGuard<User>(
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
106
|
+
createRoleGuard<User>(
|
|
107
|
+
[
|
|
108
|
+
"admin",
|
|
109
|
+
"billing",
|
|
110
|
+
],
|
|
111
|
+
{
|
|
112
|
+
match: "all",
|
|
113
|
+
}
|
|
114
|
+
);
|
|
136
115
|
```
|
|
137
116
|
|
|
138
|
-
|
|
117
|
+
## Require permissions — 0.2.6+
|
|
139
118
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
`requireRole()` and `createRoleGuard()` can read another user field and require every value:
|
|
119
|
+
Authorization & Security v2 adds a dedicated permission guard instead of requiring applications to overload role configuration.
|
|
143
120
|
|
|
144
121
|
```ts
|
|
122
|
+
import {
|
|
123
|
+
createPermissionGuard,
|
|
124
|
+
} from "bcp/auth";
|
|
125
|
+
|
|
145
126
|
interface User {
|
|
146
127
|
id: number;
|
|
147
128
|
permissions: string[];
|
|
148
129
|
}
|
|
149
130
|
|
|
150
131
|
export const guard =
|
|
151
|
-
|
|
132
|
+
createPermissionGuard<User>(
|
|
133
|
+
"users.read"
|
|
134
|
+
);
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
The default permission field is `permissions`.
|
|
138
|
+
|
|
139
|
+
Require every permission:
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
export const guard =
|
|
143
|
+
createPermissionGuard<User>(
|
|
152
144
|
[
|
|
153
145
|
"users.read",
|
|
154
146
|
"users.write",
|
|
155
147
|
],
|
|
156
148
|
{
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
149
|
+
match: "all",
|
|
150
|
+
}
|
|
151
|
+
);
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Use another user field such as `scopes`:
|
|
155
|
+
|
|
156
|
+
```ts
|
|
157
|
+
export const guard =
|
|
158
|
+
createPermissionGuard<User>(
|
|
159
|
+
"billing.read",
|
|
160
|
+
{
|
|
161
|
+
permissionField:
|
|
162
|
+
"scopes",
|
|
161
163
|
}
|
|
162
164
|
);
|
|
163
165
|
```
|
|
164
166
|
|
|
167
|
+
The low-level form is `requirePermission()`.
|
|
168
|
+
|
|
169
|
+
Authenticated users without the required permission receive `403 Forbidden` by default. Use `forbiddenRedirectTo` when the application prefers a redirect.
|
|
170
|
+
|
|
171
|
+
For authorization decisions that depend on a resource or ownership rather than a flat permission, use the policy APIs documented in [Authorization & Security v2](authorization-security.md).
|
|
172
|
+
|
|
165
173
|
## Inline guard logic
|
|
166
174
|
|
|
167
175
|
Use `requireAuth()` when a guard needs additional application-specific checks:
|
|
@@ -198,7 +206,7 @@ export async function guard() {
|
|
|
198
206
|
|
|
199
207
|
## Session-store policies apply inside guards
|
|
200
208
|
|
|
201
|
-
`requireAuth()`, `requireRole()`, and `requireGuest()` all use the same `auth()` runtime.
|
|
209
|
+
`requireAuth()`, `requireRole()`, `requirePermission()`, and `requireGuest()` all use the same `auth()` runtime.
|
|
202
210
|
|
|
203
211
|
When auth options include a server-side store and idle timeout, guard evaluation observes revocation and inactivity before allowing the route.
|
|
204
212
|
|
|
@@ -254,7 +262,7 @@ export async function loader({
|
|
|
254
262
|
|
|
255
263
|
## Standalone production behavior
|
|
256
264
|
|
|
257
|
-
`bcp/auth` stays inside the production guard/runtime request-context graph. `requireAuth()`, `requireGuest()`, `requireRole()`, `auth()`, `getSession()`, and `cookies()` therefore observe the active request after `bcp build`.
|
|
265
|
+
`bcp/auth` stays inside the production guard/runtime request-context graph. `requireAuth()`, `requireGuest()`, `requireRole()`, `requirePermission()`, `auth()`, `getSession()`, and `cookies()` therefore observe the active request after `bcp build`.
|
|
258
266
|
|
|
259
267
|
The production form-action bundle uses the same runtime boundary because guards may execute before an action.
|
|
260
268
|
|
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
# Authorization & Security v2
|
|
2
|
+
|
|
3
|
+
BCP Framework `0.2.6` adds permission- and policy-based authorization plus request-origin and CSRF protection primitives while keeping the existing authentication APIs backward compatible.
|
|
4
|
+
|
|
5
|
+
## Permission checks
|
|
6
|
+
|
|
7
|
+
Permission helpers are exported from the server-only `bcp/auth` entrypoint.
|
|
8
|
+
|
|
9
|
+
```ts
|
|
10
|
+
import {
|
|
11
|
+
hasPermission,
|
|
12
|
+
assertPermission,
|
|
13
|
+
} from "bcp/auth";
|
|
14
|
+
|
|
15
|
+
const user = {
|
|
16
|
+
id: 42,
|
|
17
|
+
permissions: [
|
|
18
|
+
"users.read",
|
|
19
|
+
"users.write",
|
|
20
|
+
],
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
hasPermission(
|
|
24
|
+
user,
|
|
25
|
+
"users.read"
|
|
26
|
+
); // true
|
|
27
|
+
|
|
28
|
+
hasPermission(
|
|
29
|
+
user,
|
|
30
|
+
[
|
|
31
|
+
"users.read",
|
|
32
|
+
"users.delete",
|
|
33
|
+
],
|
|
34
|
+
{
|
|
35
|
+
match: "all",
|
|
36
|
+
}
|
|
37
|
+
); // false
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The default permission field is `permissions`. Override it when an application uses another user shape:
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
hasPermission(
|
|
44
|
+
user,
|
|
45
|
+
"billing.read",
|
|
46
|
+
{
|
|
47
|
+
field: "scopes",
|
|
48
|
+
}
|
|
49
|
+
);
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
`assertPermission()` throws `AuthorizationError` with status `403` when the requirement is not satisfied.
|
|
53
|
+
|
|
54
|
+
## Permission route guards
|
|
55
|
+
|
|
56
|
+
Protect a route tree with a permission requirement:
|
|
57
|
+
|
|
58
|
+
```ts
|
|
59
|
+
import {
|
|
60
|
+
createPermissionGuard,
|
|
61
|
+
} from "bcp/auth";
|
|
62
|
+
|
|
63
|
+
export const guard =
|
|
64
|
+
createPermissionGuard(
|
|
65
|
+
"users.read"
|
|
66
|
+
);
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
Require every permission:
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
export const guard =
|
|
73
|
+
createPermissionGuard(
|
|
74
|
+
[
|
|
75
|
+
"users.read",
|
|
76
|
+
"users.write",
|
|
77
|
+
],
|
|
78
|
+
{
|
|
79
|
+
match: "all",
|
|
80
|
+
}
|
|
81
|
+
);
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
An unauthenticated request follows the normal `requireAuth()` behavior. An authenticated user without the required permission receives `403 Forbidden` by default.
|
|
85
|
+
|
|
86
|
+
A custom permission field and forbidden redirect are supported:
|
|
87
|
+
|
|
88
|
+
```ts
|
|
89
|
+
export const guard =
|
|
90
|
+
createPermissionGuard(
|
|
91
|
+
"admin.access",
|
|
92
|
+
{
|
|
93
|
+
permissionField:
|
|
94
|
+
"scopes",
|
|
95
|
+
forbiddenRedirectTo:
|
|
96
|
+
"/forbidden",
|
|
97
|
+
}
|
|
98
|
+
);
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
The lower-level `requirePermission()` helper is available for custom guard logic.
|
|
102
|
+
|
|
103
|
+
## Authorization policies
|
|
104
|
+
|
|
105
|
+
Policies are useful when authorization depends on a resource, ownership, tenant, state, or other application-specific data instead of a flat permission string.
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import {
|
|
109
|
+
defineAuthorizationPolicy,
|
|
110
|
+
authorize,
|
|
111
|
+
can,
|
|
112
|
+
} from "bcp/auth";
|
|
113
|
+
|
|
114
|
+
interface User {
|
|
115
|
+
id: number;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
interface Project {
|
|
119
|
+
ownerId: number;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const updateProject =
|
|
123
|
+
defineAuthorizationPolicy<
|
|
124
|
+
User,
|
|
125
|
+
Project
|
|
126
|
+
>(
|
|
127
|
+
({
|
|
128
|
+
user,
|
|
129
|
+
resource,
|
|
130
|
+
}) =>
|
|
131
|
+
resource?.ownerId ===
|
|
132
|
+
user.id
|
|
133
|
+
);
|
|
134
|
+
|
|
135
|
+
const allowed =
|
|
136
|
+
await can(
|
|
137
|
+
updateProject,
|
|
138
|
+
{
|
|
139
|
+
user,
|
|
140
|
+
resource: project,
|
|
141
|
+
}
|
|
142
|
+
);
|
|
143
|
+
|
|
144
|
+
await authorize(
|
|
145
|
+
updateProject,
|
|
146
|
+
{
|
|
147
|
+
user,
|
|
148
|
+
resource: project,
|
|
149
|
+
}
|
|
150
|
+
);
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
`cannot()` is the inverse of `can()`. `authorize()` throws `AuthorizationError` when the policy returns a falsy result.
|
|
154
|
+
|
|
155
|
+
Policies may be synchronous or asynchronous.
|
|
156
|
+
|
|
157
|
+
## Same-origin mutation protection
|
|
158
|
+
|
|
159
|
+
Request-security helpers are exported from `bcp/server`.
|
|
160
|
+
|
|
161
|
+
```ts
|
|
162
|
+
import {
|
|
163
|
+
requireSameOriginRequest,
|
|
164
|
+
} from "bcp/server";
|
|
165
|
+
|
|
166
|
+
export async function POST() {
|
|
167
|
+
await requireSameOriginRequest();
|
|
168
|
+
|
|
169
|
+
// mutation
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
For unsafe HTTP methods BCP checks `Origin` first and then `Referer`. The application request origin is allowed automatically.
|
|
174
|
+
|
|
175
|
+
Additional trusted origins can be declared explicitly:
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
await requireSameOriginRequest({
|
|
179
|
+
allowedOrigins: [
|
|
180
|
+
"https://admin.example.com",
|
|
181
|
+
],
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
Missing origin information is rejected for unsafe methods by default. Set `allowMissingOrigin: true` only when a trusted non-browser client cannot send either header and another protection is in place.
|
|
186
|
+
|
|
187
|
+
## CSRF tokens
|
|
188
|
+
|
|
189
|
+
BCP provides an HttpOnly double-submit style CSRF cookie plus a signed token returned to server code.
|
|
190
|
+
|
|
191
|
+
Create a token during page rendering or another trusted same-origin response:
|
|
192
|
+
|
|
193
|
+
```ts
|
|
194
|
+
import {
|
|
195
|
+
createCsrfToken,
|
|
196
|
+
} from "bcp/server";
|
|
197
|
+
|
|
198
|
+
const csrfToken =
|
|
199
|
+
await createCsrfToken();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
Pass the returned token to the page/form and submit it back in an application-controlled field or the default header:
|
|
203
|
+
|
|
204
|
+
```text
|
|
205
|
+
X-BCP-CSRF: <token>
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
Verify a mutation:
|
|
209
|
+
|
|
210
|
+
```ts
|
|
211
|
+
import {
|
|
212
|
+
requireCsrfRequest,
|
|
213
|
+
} from "bcp/server";
|
|
214
|
+
|
|
215
|
+
export async function POST() {
|
|
216
|
+
await requireCsrfRequest();
|
|
217
|
+
|
|
218
|
+
// protected mutation
|
|
219
|
+
}
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
When a form token is parsed from `FormData`, pass it explicitly:
|
|
223
|
+
|
|
224
|
+
```ts
|
|
225
|
+
await requireCsrfRequest({
|
|
226
|
+
token:
|
|
227
|
+
String(
|
|
228
|
+
formData.get("csrf") ??
|
|
229
|
+
""
|
|
230
|
+
),
|
|
231
|
+
});
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
`requireCsrfRequest()` combines same-origin validation and CSRF token validation for unsafe methods. Safe read requests do not require a token.
|
|
235
|
+
|
|
236
|
+
## CSRF secret
|
|
237
|
+
|
|
238
|
+
The token signer resolves secrets in this order:
|
|
239
|
+
|
|
240
|
+
```text
|
|
241
|
+
explicit options.secret
|
|
242
|
+
BCP_CSRF_SECRET
|
|
243
|
+
BCP_SESSION_SECRET
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
Secrets must contain at least 32 UTF-8 bytes.
|
|
247
|
+
|
|
248
|
+
For deployments that want separate authentication and CSRF key rotation, define:
|
|
249
|
+
|
|
250
|
+
```dotenv
|
|
251
|
+
BCP_SESSION_SECRET=...
|
|
252
|
+
BCP_CSRF_SECRET=...
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
The default CSRF cookie is:
|
|
256
|
+
|
|
257
|
+
```text
|
|
258
|
+
name bcp_csrf
|
|
259
|
+
HttpOnly true
|
|
260
|
+
SameSite Lax
|
|
261
|
+
Secure true in production
|
|
262
|
+
max age 2 hours
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
The cookie is signed but the token itself must still be treated as security-sensitive request state. Do not log CSRF tokens.
|
|
266
|
+
|
|
267
|
+
## Lower-level verification
|
|
268
|
+
|
|
269
|
+
The following helpers are available when applications need custom response behavior:
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
isSafeHttpMethod()
|
|
273
|
+
isSameOriginRequest()
|
|
274
|
+
verifyCsrfToken()
|
|
275
|
+
verifyCsrfRequest()
|
|
276
|
+
destroyCsrfToken()
|
|
277
|
+
```
|
|
278
|
+
|
|
279
|
+
`RequestSecurityError` uses status `403` and distinguishes `INVALID_ORIGIN` from `INVALID_CSRF_TOKEN`.
|
|
280
|
+
|
|
281
|
+
## Security model
|
|
282
|
+
|
|
283
|
+
Authorization and CSRF protection solve different problems:
|
|
284
|
+
|
|
285
|
+
```text
|
|
286
|
+
auth() / session
|
|
287
|
+
-> who is this user?
|
|
288
|
+
|
|
289
|
+
permission / policy
|
|
290
|
+
-> may this user perform this operation?
|
|
291
|
+
|
|
292
|
+
origin + CSRF
|
|
293
|
+
-> did this browser mutation come from an allowed application context?
|
|
294
|
+
```
|
|
295
|
+
|
|
296
|
+
Applications should still validate all mutation input and enforce authorization on the server. Client-side UI checks are only presentation logic and must not replace server authorization.
|
|
297
|
+
|
|
298
|
+
`bcp/auth` and the request-security helpers in `bcp/server` are server-only surfaces.
|
|
@@ -1,6 +1,21 @@
|
|
|
1
1
|
# Logging and observability
|
|
2
2
|
|
|
3
|
-
BCP Framework 0.1.23
|
|
3
|
+
BCP Framework 0.1.23 added the structured server logger and development request timing output. BCP Framework `0.2.7` complements that logging layer with process-local metrics and health/readiness primitives through `bcp/observability`.
|
|
4
|
+
|
|
5
|
+
Use these layers together:
|
|
6
|
+
|
|
7
|
+
```text
|
|
8
|
+
bcp/server
|
|
9
|
+
-> structured event/request logs
|
|
10
|
+
|
|
11
|
+
bcp/observability
|
|
12
|
+
-> counters, gauges, histograms
|
|
13
|
+
-> Prometheus exposition
|
|
14
|
+
-> request metrics middleware
|
|
15
|
+
-> health/readiness checks
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
See [Observability Platform v2](observability.md) for the `0.2.7` metrics and health APIs.
|
|
4
19
|
|
|
5
20
|
## Server logger
|
|
6
21
|
|
|
@@ -205,6 +220,34 @@ ssr.render
|
|
|
205
220
|
|
|
206
221
|
They are therefore visible when `BCP_LOG_LEVEL=debug` and stay out of normal `info` output.
|
|
207
222
|
|
|
223
|
+
## Metrics vs logs
|
|
224
|
+
|
|
225
|
+
Logs and metrics serve different purposes.
|
|
226
|
+
|
|
227
|
+
Use structured logs when you need per-event context:
|
|
228
|
+
|
|
229
|
+
```text
|
|
230
|
+
request ID
|
|
231
|
+
operation
|
|
232
|
+
error stack
|
|
233
|
+
resource identity
|
|
234
|
+
diagnostic fields
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
Use metrics when you need aggregate trends:
|
|
238
|
+
|
|
239
|
+
```text
|
|
240
|
+
request count
|
|
241
|
+
status distribution
|
|
242
|
+
latency buckets
|
|
243
|
+
queue depth
|
|
244
|
+
worker count
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Avoid copying high-cardinality log fields such as request IDs, user IDs or raw paths into metric labels.
|
|
248
|
+
|
|
249
|
+
The `0.2.7` request metrics middleware follows this rule by using `method` and `status` labels by default.
|
|
250
|
+
|
|
208
251
|
## Errors
|
|
209
252
|
|
|
210
253
|
Pass an `Error` as a structured field:
|