@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.
@@ -164,23 +164,33 @@ Related guides: [Database](database.md), [Database Migrations](database-migratio
164
164
 
165
165
  ## `bcp/auth`
166
166
 
167
- Server-only Authentication Platform v2 APIs.
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
- `logoutAll()`, `revokeSession()`, and `revokeUserSessions()` require server-side session state. `requireGuest()` / `createGuestGuard()` support login/register routes that should redirect already-authenticated users.
219
+ Authorization & Security v2 adds permission checks, permission route guards and resource-aware policy functions:
204
220
 
205
- Related guides: [Authentication](authentication.md), [Auth Session Stores](auth-session-store.md), [Auth Route Guards](auth-route-guards.md), [JWT Sessions](session-auth.md).
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
- Related guides: [Server Request APIs](server-request-apis.md), [File Upload](file-upload.md), [Storage](storage.md), [Storage Ecosystem](storage-ecosystem.md), [Production Hardening](production-hardening.md).
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
- ```ts
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
- You can redirect forbidden users instead:
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
- forbiddenRedirectTo:
121
- "/forbidden",
122
- }
123
- );
98
+ "manager",
99
+ ]);
124
100
  ```
125
101
 
126
- ## Require one of several roles
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
- "admin",
134
- "manager",
135
- ]);
106
+ createRoleGuard<User>(
107
+ [
108
+ "admin",
109
+ "billing",
110
+ ],
111
+ {
112
+ match: "all",
113
+ }
114
+ );
136
115
  ```
137
116
 
138
- A user with either role is allowed.
117
+ ## Require permissions 0.2.6+
139
118
 
140
- ## Require all permissions
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
- createRoleGuard<User>(
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
- roleField:
158
- "permissions",
159
- match:
160
- "all",
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 adds a structured server logger and upgrades development request timing output while keeping framework-internal browser/bootstrap traffic quiet by default.
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: