@basaltkit/express 1.4.0 → 1.4.1

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.
Files changed (2) hide show
  1. package/README.md +56 -5
  2. package/package.json +7 -3
package/README.md CHANGED
@@ -14,7 +14,7 @@ Basalt adapter for [Express](https://expressjs.com): the same typed routes, enri
14
14
 
15
15
  This module connects Express to Basalt. **Routes** (address + method, e.g. `POST /echo`) are defined with the `route()` function from `@basaltkit/http`, in a neutral format with [Zod](https://zod.dev) schemas for validation. The adapter converts each Express request into that neutral format, runs the shared pipeline (validation, *enrichers* — functions that enrich the request context, like resolving the tenant — and *guards* — functions that can reject the request, like authentication), and converts errors into JSON responses with a stable format.
16
16
 
17
- The strong point: **portability**. A route written for this adapter runs unchanged on `@basaltkit/fastify` and `@basaltkit/hono`. And the neutral edge plugins (security, health, metrics, tracing, OpenAPI) from `@basaltkit/http` work here exactly the same.
17
+ The strong point: **portability**. A route written for this adapter runs unchanged on `@basaltkit/fastify` and `@basaltkit/hono` **the three adapters are equals**. Routes, enrichers, guards, the per-request context, standardized errors, the neutral 404, ETags (`meta.etag`), SSE, per-route rate limits and the boot-time guarded-meta check all behave identically. The neutral edge plugins (security, health, metrics, tracing, OpenAPI) from `@basaltkit/http` work here exactly the same.
18
18
 
19
19
  ## Installation
20
20
 
@@ -70,7 +70,7 @@ curl -X POST http://localhost:3000/echo \
70
70
  # → 400 {"error":{"code":"HTTP_VALIDATION","part":"body","issues":[...]}}
71
71
  ```
72
72
 
73
- > The plugin already enables `express.json()` for you — you don't need to configure JSON parsing.
73
+ > The plugin enables `express.json()` **and** `express.urlencoded({ extended: false })` for you — you don't need to configure body parsing. (The urlencoded parser is what makes HTML forms and the SAML ACS binding work.)
74
74
 
75
75
  ## Usage guide
76
76
 
@@ -101,6 +101,39 @@ const boom = route({
101
101
 
102
102
  The error format is identical to the other adapters: `{ error: { code, message, ... } }`. Unexpected errors respond with `500` and `INTERNAL_ERROR`, without exposing internal details.
103
103
 
104
+ ### Guarded route meta — the boot check
105
+
106
+ If a route declares `meta.auth`, `meta.can` or `meta.teamRole` and **no registered plugin
107
+ enforces that key**, the route would serve unprotected. `expressPlugin` refuses to boot:
108
+ it calls `assertRoutesGuarded()` in its boot phase and throws `UnguardedRouteMetaError`
109
+ (code `HTTP_UNGUARDED_ROUTE_META`), naming every offending route and key, before a single
110
+ request is served.
111
+
112
+ Fix it by registering the enforcing plugin (`auth` → `authPlugin`, `can` →
113
+ `permissionsPlugin`, `teamRole` → `teamsPlugin`). If protection really does happen at an
114
+ outer edge (a gateway that authenticates first), waive it explicitly:
115
+
116
+ ```ts
117
+ expressPlugin({ routes, allowUnguardedMeta: true }) // waive every key
118
+ expressPlugin({ routes, allowUnguardedMeta: ['auth'] }) // waive one key
119
+ ```
120
+
121
+ Fastify and Hono run the identical check with the identical option.
122
+
123
+ ### The neutral 404
124
+
125
+ Unmatched routes get the same JSON body on every adapter —
126
+ `{ "error": { "code": "NOT_FOUND", "message": "Route not found." } }` (`NOT_FOUND_RESPONSE`
127
+ from `@basaltkit/http`) — instead of Express's HTML default, which fingerprints the
128
+ framework. It is mounted last, at `app:booted`. Pass `notFound: false` to keep Express's
129
+ own handling, e.g. when your app mounts its own catch-all afterwards.
130
+
131
+ ### Streaming — SSE
132
+
133
+ A handler returning `sse(producer)` from `@basaltkit/http` is streamed straight onto the
134
+ Express response (`res.writeHead(200, SSE_HEADERS)`), with client disconnects relayed to
135
+ `stream.onClose()`. Same handler code as on Fastify and Hono.
136
+
104
137
  ### Enrichers and guards (authentication, tenancy, …)
105
138
 
106
139
  Plugins register these functions in the container's metadata "buckets"; the adapter applies them to every route. Real example (from the package's tests) — a tenancy-style enricher and an auth-style guard:
@@ -213,11 +246,26 @@ In this mode each handler already handles its own errors (the wrapper responds w
213
246
  |---|---|---|---|---|
214
247
  | `routes` | `BasaltRoute[]` | No | `[]` | Routes (created with `route()` from `@basaltkit/http`) to mount. |
215
248
  | `allowUnguardedMeta` | `boolean \| string[]` | No | fail loud at boot | Waives the boot check that every route declaring security meta (`auth`, `can`, `teamRole`) has a registered guard enforcing it (`UnguardedRouteMetaError` otherwise). `true` waives everything (edge/gateway auth); an array waives specific keys. |
216
- | `app` | `Express` | No | new `express()` | Bring your own Express app; either way, `express.json()` is added. |
249
+ | `app` | `Express` | No | new `express()` | Bring your own Express app; either way, `express.json()` and `express.urlencoded({ extended: false })` are added. |
250
+ | `notFound` | `boolean` | No | `true` | Serve `NOT_FOUND_RESPONSE` (the neutral JSON 404) for unmatched routes, mounted last. Set `false` to keep Express's HTML default or your own catch-all. |
217
251
 
218
252
  Behavior: registers the Express app under the `EXPRESS` token and an `HttpServerCollector` under the `HTTP_SERVER` token. On the `app:booted` event it mounts everything in the order Express requires: *after-hooks* middleware (metrics/tracing, via `res.on('finish')`) → *pre-hooks* middleware (security/CORS/rate limit; if one of them responds, the route doesn't run) → Basalt routes → extra routes from edge plugins (`/livez`, `/metrics`, …). Publishes the routes in the `'http:routes'` metadata bucket for OpenAPI/CLI/SDK.
219
253
 
220
- > Note: unlike `fastifyPlugin`, this plugin has no `shutdown` step — closing the HTTP server returned by `listen()` is your responsibility.
254
+ > Note: unlike `fastifyPlugin`, this plugin has no `shutdown` step — Basalt never calls `listen()` for you, so closing the server it returns is your responsibility.
255
+
256
+ ### Errors
257
+
258
+ | Error | Code | HTTP | When |
259
+ |---|---|---|---|
260
+ | `RequestValidationError` | `HTTP_VALIDATION` | 400 | `body`/`query`/`params` failed its Zod schema. Response carries `part` + `issues[]`. |
261
+ | `HttpError(status, code, message)` | *yours* | *yours* | Thrown deliberately from any layer. |
262
+ | `UnguardedRouteMetaError` | `HTTP_UNGUARDED_ROUTE_META` | — (boot) | A route declares a guarded key (`auth`/`can`/`teamRole`/`scopes`/`subscribed`/`feature`) with no guard enforcing it. Waive with `allowUnguardedMeta`. |
263
+ | — | `NOT_FOUND` | 404 | No route matched (unless `notFound: false`). |
264
+ | — | `RATE_LIMITED` | 429 | `securityPlugin`'s limiter rejected the request. |
265
+ | — | `INTERNAL_ERROR` | 500 | Any other thrown error. The real message never reaches the client. |
266
+
267
+ All of these are produced by the shared `@basaltkit/http` pipeline, so the bodies are
268
+ byte-identical to Fastify's and Hono's.
221
269
 
222
270
  ### `EXPRESS`
223
271
 
@@ -235,7 +283,7 @@ Dependency-injection token (`Token<Express>`): `app.container.get(EXPRESS)` retu
235
283
 
236
284
  ### What to import from where
237
285
 
238
- This package only exports `expressPlugin`, `registerRoutes`, `EXPRESS`, and `ExpressPluginOptions`. Everything else — `route`, `HttpError`, `RequestValidationError`, `securityPlugin`, `healthPlugin`, `metricsPlugin`, `tracingPlugin`, `openapiPlugin`, types like `RequestEnricher`/`RouteGuard` — is imported from **`@basaltkit/http`**.
286
+ This package only exports `expressPlugin`, `registerRoutes`, `EXPRESS`, and `ExpressPluginOptions`. Everything else — `route`, `HttpError`, `RequestValidationError`, `NOT_FOUND_RESPONSE`, `sse`, `securityPlugin`, `RedisRateLimitStore`, `healthPlugin`, `metricsPlugin`, `tracingPlugin`, `openapiPlugin`, `escapeHtml`/`pageCsp`, types like `RequestEnricher`/`RouteGuard` — is imported from **`@basaltkit/http`**. (Unlike `@basaltkit/fastify`, this package re-exports nothing; that is a naming choice, not a capability gap.)
239
287
 
240
288
  ## Common errors and solutions (FAQ)
241
289
 
@@ -258,3 +306,6 @@ This package only exports `expressPlugin`, `registerRoutes`, `EXPRESS`, and `Exp
258
306
  - **`@basaltkit/fastify` / `@basaltkit/hono`** — sibling adapters: the same routes, enrichers, guards, and edge plugins run on any of them unchanged; switching frameworks is just switching plugins.
259
307
  - **`@basaltkit/auth` / `@basaltkit/tenancy` / `@basaltkit/permissions`** — register guards/enrichers in `'http:guards'`/`'http:enrichers'` and read the routes' `meta` (e.g. `meta: { auth: true }`); this adapter applies them automatically.
260
308
  - **`@basaltkit/sdk` and the CLI** — consume the `'http:routes'` bucket (routes + Zod schemas) that this plugin publishes.
309
+ - **`@basaltkit/testing`** — `createTestApp({ adapter: 'express' })` runs your suite against this adapter (it listens on an ephemeral 127.0.0.1 port and fetches, because Express has no in-process inject).
310
+
311
+ Guides: [Adapters](/guide/adapters) · [Migrating from Express](/guide/migrating-from-express) · [Testing](/guide/testing) · [Security](/guide/security)
package/package.json CHANGED
@@ -1,9 +1,13 @@
1
1
  {
2
2
  "name": "@basaltkit/express",
3
- "version": "1.4.0",
3
+ "version": "1.4.1",
4
+ "engines": {
5
+ "node": ">=22.5.0"
6
+ },
4
7
  "description": "Express adapter for Basalt: run the same typed routes, enrichers and guards on Express that you would on Fastify or Hono.",
5
8
  "license": "MIT",
6
9
  "type": "module",
10
+ "sideEffects": false,
7
11
  "exports": {
8
12
  ".": {
9
13
  "types": "./dist/index.d.ts",
@@ -14,8 +18,8 @@
14
18
  "dist"
15
19
  ],
16
20
  "dependencies": {
17
- "@basaltkit/core": "^1.3.0",
18
- "@basaltkit/http": "^1.12.0"
21
+ "@basaltkit/core": "^1.3.1",
22
+ "@basaltkit/http": "^1.14.0"
19
23
  },
20
24
  "peerDependencies": {
21
25
  "express": "^4.19.0 || ^5.0.0"