@basaltkit/express 1.3.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.
- package/README.md +57 -5
- package/dist/index.d.ts +7 -0
- package/dist/index.js +4 -1
- 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
|
|
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
|
|
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:
|
|
@@ -212,11 +245,27 @@ In this mode each handler already handles its own errors (the wrapper responds w
|
|
|
212
245
|
| Option | Type | Required? | Default | Description |
|
|
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. |
|
|
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. |
|
|
216
251
|
|
|
217
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.
|
|
218
253
|
|
|
219
|
-
> Note: unlike `fastifyPlugin`, this plugin has no `shutdown` step —
|
|
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.
|
|
220
269
|
|
|
221
270
|
### `EXPRESS`
|
|
222
271
|
|
|
@@ -234,7 +283,7 @@ Dependency-injection token (`Token<Express>`): `app.container.get(EXPRESS)` retu
|
|
|
234
283
|
|
|
235
284
|
### What to import from where
|
|
236
285
|
|
|
237
|
-
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.)
|
|
238
287
|
|
|
239
288
|
## Common errors and solutions (FAQ)
|
|
240
289
|
|
|
@@ -257,3 +306,6 @@ This package only exports `expressPlugin`, `registerRoutes`, `EXPRESS`, and `Exp
|
|
|
257
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.
|
|
258
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.
|
|
259
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/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,13 @@ export declare const EXPRESS: import("@basaltkit/core").Token<Express>;
|
|
|
6
6
|
export declare function registerRoutes(app: Express, routes: BasaltRoute[], container?: Container, enrichers?: RequestEnricher[], guards?: RouteGuard[]): void;
|
|
7
7
|
export interface ExpressPluginOptions {
|
|
8
8
|
routes?: BasaltRoute[];
|
|
9
|
+
/**
|
|
10
|
+
* Waives the boot-time check that every route declaring security meta
|
|
11
|
+
* (`auth`, `can`, `teamRole`) has a registered guard enforcing it. Pass
|
|
12
|
+
* `true` to waive everything (e.g. authentication handled at an outer
|
|
13
|
+
* edge/gateway), or an array of specific keys. Default: fail loud at boot.
|
|
14
|
+
*/
|
|
15
|
+
allowUnguardedMeta?: boolean | string[];
|
|
9
16
|
/** Bring your own Express app; otherwise one is created with `express.json()`. */
|
|
10
17
|
app?: Express;
|
|
11
18
|
/**
|
package/dist/index.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Container, createToken, definePlugin, ensureMetadata } from '@basaltkit/core';
|
|
2
|
-
import { NOT_FOUND_RESPONSE, HttpServerCollector, HTTP_SERVER, runRoute, toErrorResponse, isSseResponse, sseProducerOf, driveSse, SSE_HEADERS, } from '@basaltkit/http';
|
|
2
|
+
import { NOT_FOUND_RESPONSE, HttpServerCollector, HTTP_SERVER, runRoute, toErrorResponse, isSseResponse, sseProducerOf, driveSse, SSE_HEADERS, GUARDED_META_BUCKET, assertRoutesGuarded, } from '@basaltkit/http';
|
|
3
3
|
import express, {} from 'express';
|
|
4
4
|
export const EXPRESS = createToken('express');
|
|
5
5
|
function toNeutralRequest(req) {
|
|
@@ -112,6 +112,9 @@ export function expressPlugin(options = {}) {
|
|
|
112
112
|
const metadata = ensureMetadata(container);
|
|
113
113
|
const enrichers = metadata.get('http:enrichers');
|
|
114
114
|
const guards = metadata.get('http:guards');
|
|
115
|
+
// Fail loud BEFORE traffic if a route declares security meta (auth/can/
|
|
116
|
+
// teamRole) that no registered guard enforces — it would serve open.
|
|
117
|
+
assertRoutesGuarded(routes, new Set(metadata.get(GUARDED_META_BUCKET)), options.allowUnguardedMeta);
|
|
115
118
|
const router = app;
|
|
116
119
|
// Mount everything once edge plugins have registered their hooks/routes,
|
|
117
120
|
// in the order Express needs: after-hooks → pre-hooks → routes.
|
package/package.json
CHANGED
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@basaltkit/express",
|
|
3
|
-
"version": "1.
|
|
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.1
|
|
18
|
-
"@basaltkit/http": "^1.
|
|
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"
|