@wooksjs/event-http 0.6.2 → 0.6.4
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 +24 -0
- package/dist/index.cjs +132 -25
- package/dist/index.d.ts +110 -12
- package/dist/index.mjs +132 -25
- package/package.json +45 -37
- package/scripts/setup-skills.js +77 -0
- package/skills/wooksjs-event-http/SKILL.md +37 -0
- package/skills/wooksjs-event-http/addons.md +307 -0
- package/skills/wooksjs-event-http/core.md +297 -0
- package/skills/wooksjs-event-http/error-handling.md +253 -0
- package/skills/wooksjs-event-http/event-core.md +562 -0
- package/skills/wooksjs-event-http/request.md +220 -0
- package/skills/wooksjs-event-http/response.md +336 -0
- package/skills/wooksjs-event-http/routing.md +412 -0
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
# Addons — Body Parser, Static Files, Proxy
|
|
2
|
+
|
|
3
|
+
> Covers the three official addon packages: `@wooksjs/http-body` for parsing request bodies, `@wooksjs/http-static` for serving static files, and `@wooksjs/http-proxy` for proxying requests.
|
|
4
|
+
|
|
5
|
+
## Body Parser — `@wooksjs/http-body`
|
|
6
|
+
|
|
7
|
+
### Installation
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
npm install @wooksjs/http-body
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
### `useBody()`
|
|
14
|
+
|
|
15
|
+
Composable that provides content-type detection and body parsing:
|
|
16
|
+
|
|
17
|
+
```ts
|
|
18
|
+
import { useBody } from '@wooksjs/http-body'
|
|
19
|
+
|
|
20
|
+
app.post('/api/data', async () => {
|
|
21
|
+
const { parseBody, isJson, isFormData } = useBody()
|
|
22
|
+
|
|
23
|
+
if (isJson()) {
|
|
24
|
+
const data = await parseBody<{ name: string; age: number }>()
|
|
25
|
+
return { received: data }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (isFormData()) {
|
|
29
|
+
const formData = await parseBody<Record<string, string>>()
|
|
30
|
+
return { fields: formData }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return { error: 'Unsupported content type' }
|
|
34
|
+
})
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Content-type checkers
|
|
38
|
+
|
|
39
|
+
All checkers are lazy-computed and cached:
|
|
40
|
+
|
|
41
|
+
| Method | Checks for |
|
|
42
|
+
|--------|-----------|
|
|
43
|
+
| `isJson()` | `application/json` |
|
|
44
|
+
| `isHtml()` | `text/html` |
|
|
45
|
+
| `isXml()` | `text/xml` |
|
|
46
|
+
| `isText()` | `text/plain` |
|
|
47
|
+
| `isBinary()` | `application/octet-stream` |
|
|
48
|
+
| `isFormData()` | `multipart/form-data` |
|
|
49
|
+
| `isUrlencoded()` | `application/x-www-form-urlencoded` |
|
|
50
|
+
|
|
51
|
+
### `parseBody<T>()`
|
|
52
|
+
|
|
53
|
+
Parses the request body based on content type. Returns a promise. The result is cached — calling `parseBody()` twice returns the same promise.
|
|
54
|
+
|
|
55
|
+
| Content-Type | Parsed as |
|
|
56
|
+
|-------------|-----------|
|
|
57
|
+
| `application/json` | Parsed JSON object (safe parse with prototype pollution protection) |
|
|
58
|
+
| `multipart/form-data` | Parsed form fields as `Record<string, unknown>` |
|
|
59
|
+
| `application/x-www-form-urlencoded` | Parsed as object using `WooksURLSearchParams` |
|
|
60
|
+
| everything else | Raw text string |
|
|
61
|
+
|
|
62
|
+
### `rawBody()`
|
|
63
|
+
|
|
64
|
+
Also re-exports `rawBody()` from `useRequest()` for convenience:
|
|
65
|
+
|
|
66
|
+
```ts
|
|
67
|
+
const { rawBody } = useBody()
|
|
68
|
+
const buffer = await rawBody() // Buffer (decompressed if needed)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
### Common Pattern: JSON API endpoint
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
app.post('/api/users', async () => {
|
|
75
|
+
const { parseBody, isJson } = useBody()
|
|
76
|
+
if (!isJson()) throw new HttpError(415, 'Expected JSON')
|
|
77
|
+
|
|
78
|
+
const body = await parseBody<{ name: string; email: string }>()
|
|
79
|
+
const user = await db.createUser(body)
|
|
80
|
+
|
|
81
|
+
const { status } = useResponse()
|
|
82
|
+
status(201)
|
|
83
|
+
return user
|
|
84
|
+
})
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### Safety
|
|
88
|
+
|
|
89
|
+
- JSON parsing uses `safeJsonParse` which protects against `__proto__` and `constructor` pollution.
|
|
90
|
+
- Form-data parsing limits: max 255 fields, max 100-byte key names, max 100 KB per field value.
|
|
91
|
+
- Illegal keys (`__proto__`, `constructor`, `prototype`) are rejected with 400.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## Static Files — `@wooksjs/http-static`
|
|
96
|
+
|
|
97
|
+
### Installation
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
npm install @wooksjs/http-static
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### `serveFile(filePath, options?)`
|
|
104
|
+
|
|
105
|
+
Serves a static file with full HTTP caching support (ETags, If-None-Match, If-Modified-Since, Range requests):
|
|
106
|
+
|
|
107
|
+
```ts
|
|
108
|
+
import { serveFile } from '@wooksjs/http-static'
|
|
109
|
+
|
|
110
|
+
app.get('/static/*', () => {
|
|
111
|
+
const { get } = useRouteParams<{ '*': string }>()
|
|
112
|
+
return serveFile(get('*'), {
|
|
113
|
+
baseDir: './public',
|
|
114
|
+
cacheControl: { maxAge: '1h', public: true },
|
|
115
|
+
})
|
|
116
|
+
})
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Options
|
|
120
|
+
|
|
121
|
+
```ts
|
|
122
|
+
interface TServeFileOptions {
|
|
123
|
+
baseDir?: string // base directory for file resolution
|
|
124
|
+
headers?: Record<string, string> // additional response headers
|
|
125
|
+
cacheControl?: TCacheControl // Cache-Control directives
|
|
126
|
+
expires?: Date | string | number // Expires header
|
|
127
|
+
pragmaNoCache?: boolean // set Pragma: no-cache
|
|
128
|
+
defaultExt?: string // default extension (e.g. 'html')
|
|
129
|
+
index?: string // index file (e.g. 'index.html')
|
|
130
|
+
listDirectory?: boolean // serve directory listing
|
|
131
|
+
allowDotDot?: boolean // allow ../ traversal (default: false)
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Features
|
|
136
|
+
|
|
137
|
+
- **ETag & conditional requests**: Automatically generates ETags from inode/size/mtime. Returns 304 Not Modified when appropriate.
|
|
138
|
+
- **Range requests**: Supports `Range` header for partial content (206) and `If-Range` validation.
|
|
139
|
+
- **MIME type detection**: Automatically sets `Content-Type` based on file extension.
|
|
140
|
+
- **Directory listing**: Optional HTML directory listing with file sizes and dates.
|
|
141
|
+
- **Index files**: Serves `index.html` (or configured index) when a directory is requested.
|
|
142
|
+
- **Path traversal protection**: Rejects `../` by default.
|
|
143
|
+
|
|
144
|
+
### Common Pattern: SPA with fallback
|
|
145
|
+
|
|
146
|
+
```ts
|
|
147
|
+
app.get('/*', async () => {
|
|
148
|
+
const { get } = useRouteParams<{ '*': string }>()
|
|
149
|
+
try {
|
|
150
|
+
return await serveFile(get('*') || 'index.html', {
|
|
151
|
+
baseDir: './dist',
|
|
152
|
+
index: 'index.html',
|
|
153
|
+
cacheControl: { maxAge: '1d', public: true },
|
|
154
|
+
})
|
|
155
|
+
} catch {
|
|
156
|
+
// Fallback to index.html for SPA client-side routing
|
|
157
|
+
return await serveFile('index.html', { baseDir: './dist' })
|
|
158
|
+
}
|
|
159
|
+
})
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
### Common Pattern: File server with directory listing
|
|
163
|
+
|
|
164
|
+
```ts
|
|
165
|
+
app.get('/files/*', () => {
|
|
166
|
+
const { get } = useRouteParams<{ '*': string }>()
|
|
167
|
+
return serveFile(get('*') || '.', {
|
|
168
|
+
baseDir: '/var/shared',
|
|
169
|
+
listDirectory: true,
|
|
170
|
+
cacheControl: { noCache: true },
|
|
171
|
+
})
|
|
172
|
+
})
|
|
173
|
+
```
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## HTTP Proxy — `@wooksjs/http-proxy`
|
|
178
|
+
|
|
179
|
+
### Installation
|
|
180
|
+
|
|
181
|
+
```bash
|
|
182
|
+
npm install @wooksjs/http-proxy
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
### `useProxy()`
|
|
186
|
+
|
|
187
|
+
Composable that returns a `proxy()` function for forwarding the current request:
|
|
188
|
+
|
|
189
|
+
```ts
|
|
190
|
+
import { useProxy } from '@wooksjs/http-proxy'
|
|
191
|
+
|
|
192
|
+
app.get('/api/*', async () => {
|
|
193
|
+
const proxy = useProxy()
|
|
194
|
+
return await proxy('https://backend.example.com/api/endpoint', {
|
|
195
|
+
reqHeaders: { allow: '*' },
|
|
196
|
+
resHeaders: { allow: '*' },
|
|
197
|
+
})
|
|
198
|
+
})
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### `proxy(target, options?)`
|
|
202
|
+
|
|
203
|
+
The function returned by `useProxy()`:
|
|
204
|
+
|
|
205
|
+
```ts
|
|
206
|
+
async function proxy(target: string, opts?: TWooksProxyOptions): Promise<Response>
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
- `target` — Full URL to proxy to (including path and query string).
|
|
210
|
+
- Returns a `Response` (Fetch API) that the framework sends as the final response.
|
|
211
|
+
|
|
212
|
+
### Proxy Options
|
|
213
|
+
|
|
214
|
+
```ts
|
|
215
|
+
interface TWooksProxyOptions {
|
|
216
|
+
method?: string // override HTTP method
|
|
217
|
+
reqHeaders?: TWooksProxyControls // filter/transform request headers
|
|
218
|
+
reqCookies?: TWooksProxyControls // filter/transform request cookies
|
|
219
|
+
resHeaders?: TWooksProxyControls // filter/transform response headers
|
|
220
|
+
resCookies?: TWooksProxyControls // filter/transform response cookies
|
|
221
|
+
debug?: boolean // log proxy details
|
|
222
|
+
}
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Proxy Controls
|
|
226
|
+
|
|
227
|
+
Each control object (`TWooksProxyControls`) lets you allow, block, or overwrite headers/cookies:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
interface TWooksProxyControls {
|
|
231
|
+
allow?: Array<string | RegExp> | '*' // allowlist (use '*' for all)
|
|
232
|
+
block?: Array<string | RegExp> | '*' // blocklist (use '*' for none)
|
|
233
|
+
overwrite?: Record<string, string> | ((data: Record<string, string>) => Record<string, string>)
|
|
234
|
+
}
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
### Common Pattern: API Gateway
|
|
238
|
+
|
|
239
|
+
```ts
|
|
240
|
+
app.all('/api/*', async () => {
|
|
241
|
+
const proxy = useProxy()
|
|
242
|
+
const { get } = useRouteParams<{ '*': string }>()
|
|
243
|
+
const { rawSearchParams } = useSearchParams()
|
|
244
|
+
|
|
245
|
+
const targetPath = get('*')
|
|
246
|
+
const query = rawSearchParams()
|
|
247
|
+
const target = `https://internal-api.local/${targetPath}${query}`
|
|
248
|
+
|
|
249
|
+
return await proxy(target, {
|
|
250
|
+
reqHeaders: {
|
|
251
|
+
allow: '*',
|
|
252
|
+
overwrite: { 'x-gateway': 'wooks' },
|
|
253
|
+
},
|
|
254
|
+
resHeaders: { allow: '*' },
|
|
255
|
+
reqCookies: { allow: ['session_id'] },
|
|
256
|
+
resCookies: { allow: ['session_id'] },
|
|
257
|
+
})
|
|
258
|
+
})
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
### Common Pattern: Selective header forwarding
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
return await proxy('https://api.example.com/data', {
|
|
265
|
+
reqHeaders: {
|
|
266
|
+
allow: ['authorization', 'content-type', 'accept'],
|
|
267
|
+
},
|
|
268
|
+
resHeaders: {
|
|
269
|
+
allow: '*',
|
|
270
|
+
block: ['x-internal-header', 'server'],
|
|
271
|
+
},
|
|
272
|
+
})
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### Blocked headers by default
|
|
276
|
+
|
|
277
|
+
When using proxy controls, these headers are blocked by default:
|
|
278
|
+
|
|
279
|
+
**Request**: `connection`, `accept-encoding`, `content-length`, `upgrade-insecure-requests`, `cookie`
|
|
280
|
+
|
|
281
|
+
**Response**: `transfer-encoding`, `content-encoding`, `set-cookie`
|
|
282
|
+
|
|
283
|
+
To forward cookies, explicitly configure `reqCookies` / `resCookies`.
|
|
284
|
+
|
|
285
|
+
### Debug mode
|
|
286
|
+
|
|
287
|
+
```ts
|
|
288
|
+
return await proxy('https://api.example.com/data', {
|
|
289
|
+
debug: true, // logs request/response details to the event logger
|
|
290
|
+
reqHeaders: { allow: '*' },
|
|
291
|
+
resHeaders: { allow: '*' },
|
|
292
|
+
})
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
## Best Practices
|
|
296
|
+
|
|
297
|
+
- **Always validate and sanitize paths before `serveFile()`** — Don't pass user input directly. Use `baseDir` to restrict the file root.
|
|
298
|
+
- **Use `parseBody()` only once** — It reads and caches the body. Multiple calls return the same cached result.
|
|
299
|
+
- **Use proxy controls to limit header forwarding** — Don't blindly forward all headers between services. Use `allow`/`block` to be explicit.
|
|
300
|
+
- **Set `allow: '*'` explicitly** — Without controls, no headers are forwarded by the proxy.
|
|
301
|
+
|
|
302
|
+
## Gotchas
|
|
303
|
+
|
|
304
|
+
- `serveFile()` throws `HttpError(404)` if the file doesn't exist. Catch it if you need fallback behavior.
|
|
305
|
+
- `serveFile()` rejects paths containing `/../` by default. Set `allowDotDot: true` only if you're certain the path is safe.
|
|
306
|
+
- `useProxy()` uses the global `fetch()` API — ensure your Node.js version supports it (18+).
|
|
307
|
+
- The proxy function returns a `Response` object. The framework handles streaming it to the client.
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
# Core Concepts — @wooksjs/event-http
|
|
2
|
+
|
|
3
|
+
> Covers HTTP app creation, server setup, how the HTTP adapter integrates with the event context system, testing, and logging.
|
|
4
|
+
|
|
5
|
+
For the underlying event context store API (`init`, `get`, `set`, `hook`, etc.) and how to create custom composables, see [event-core.md](event-core.md).
|
|
6
|
+
|
|
7
|
+
## Mental Model
|
|
8
|
+
|
|
9
|
+
`@wooksjs/event-http` is the HTTP adapter for Wooks. It turns every incoming HTTP request into an event with its own isolated context store. Instead of middleware chains and mutated `req`/`res` objects, you call composable functions (`useRequest()`, `useCookies()`, `useSetHeaders()`, etc.) from anywhere in your handler — values are computed on demand and cached per request.
|
|
10
|
+
|
|
11
|
+
Key principles:
|
|
12
|
+
1. **Never mutate `req`** — Accumulate request context in the store instead.
|
|
13
|
+
2. **Never parse before needed** — Cookies, body, search params are only parsed when a composable is first called.
|
|
14
|
+
3. **No middleware sprawl** — Composable functions replace middleware. Each one is a focused, importable utility.
|
|
15
|
+
|
|
16
|
+
## Installation
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install wooks @wooksjs/event-http
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
## Creating an HTTP App
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
import { createHttpApp } from '@wooksjs/event-http'
|
|
26
|
+
|
|
27
|
+
const app = createHttpApp()
|
|
28
|
+
|
|
29
|
+
app.get('/hello', () => 'Hello World!')
|
|
30
|
+
|
|
31
|
+
app.listen(3000, () => {
|
|
32
|
+
console.log('Server running on port 3000')
|
|
33
|
+
})
|
|
34
|
+
```
|
|
35
|
+
|
|
36
|
+
`createHttpApp(opts?, wooks?)` returns a `WooksHttp` instance. Options:
|
|
37
|
+
|
|
38
|
+
```ts
|
|
39
|
+
interface TWooksHttpOptions {
|
|
40
|
+
logger?: TConsoleBase // custom logger
|
|
41
|
+
eventOptions?: TEventOptions // event-level logger config
|
|
42
|
+
onNotFound?: TWooksHandler // custom 404 handler
|
|
43
|
+
router?: {
|
|
44
|
+
ignoreTrailingSlash?: boolean // treat /path and /path/ as the same
|
|
45
|
+
ignoreCase?: boolean // case-insensitive matching
|
|
46
|
+
cacheLimit?: number // max cached parsed routes
|
|
47
|
+
}
|
|
48
|
+
requestLimits?: { // app-level body limits (overridable per-request)
|
|
49
|
+
maxCompressed?: number // default: 1 MB
|
|
50
|
+
maxInflated?: number // default: 10 MB
|
|
51
|
+
maxRatio?: number // default: 100 (zip-bomb protection)
|
|
52
|
+
readTimeoutMs?: number // default: 10 000 ms
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Example — raise body limits for the entire app:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
const app = createHttpApp({
|
|
61
|
+
requestLimits: {
|
|
62
|
+
maxCompressed: 50 * 1024 * 1024, // 50 MB
|
|
63
|
+
maxInflated: 100 * 1024 * 1024, // 100 MB
|
|
64
|
+
maxRatio: 200,
|
|
65
|
+
},
|
|
66
|
+
})
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
These defaults apply to every request but can still be overridden per-request via `useRequest()` (see [request.md](request.md)).
|
|
70
|
+
|
|
71
|
+
## Using with an Existing Node.js Server
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
import http from 'http'
|
|
75
|
+
import { createHttpApp } from '@wooksjs/event-http'
|
|
76
|
+
|
|
77
|
+
const app = createHttpApp()
|
|
78
|
+
app.get('/hello', () => 'Hello World!')
|
|
79
|
+
|
|
80
|
+
const server = http.createServer(app.getServerCb())
|
|
81
|
+
server.listen(3000)
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## How HTTP Context Works
|
|
85
|
+
|
|
86
|
+
When a request arrives, the adapter creates an HTTP-specific event context:
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
Request arrives (req, res)
|
|
90
|
+
→ createHttpContext({ req, res }, options)
|
|
91
|
+
→ AsyncLocalStorage.run(httpContextStore, handler)
|
|
92
|
+
→ router matches path → handler runs
|
|
93
|
+
→ handler calls useRequest(), useCookies(), etc.
|
|
94
|
+
→ each composable calls useHttpContext()
|
|
95
|
+
→ reads/writes the HTTP context store via init(), get(), set()
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
### The HTTP Context Store
|
|
99
|
+
|
|
100
|
+
The HTTP adapter extends the base event context with these sections:
|
|
101
|
+
|
|
102
|
+
```ts
|
|
103
|
+
interface THttpContextStore {
|
|
104
|
+
searchParams?: TSearchParamsCache // cached query params
|
|
105
|
+
cookies?: Record<string, string | null> // cached parsed cookies
|
|
106
|
+
setCookies?: Record<string, TSetCookieData> // outgoing response cookies
|
|
107
|
+
accept?: Record<string, boolean> // cached Accept header checks
|
|
108
|
+
authorization?: TAuthCache // cached auth header parsing
|
|
109
|
+
setHeader?: Record<string, string | string[]> // outgoing response headers
|
|
110
|
+
request?: TRequestCache // cached request data (body, IP, etc.)
|
|
111
|
+
response?: { responded: boolean } // response sent flag
|
|
112
|
+
status?: { code: EHttpStatusCode } // response status code
|
|
113
|
+
}
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
Every section is lazily initialized — it only exists when a composable first writes to it. This means zero overhead for unused features.
|
|
117
|
+
|
|
118
|
+
### Extending the HTTP Store for Custom Composables
|
|
119
|
+
|
|
120
|
+
When creating custom composables for HTTP, extend the store type via the generic parameter on `useHttpContext`:
|
|
121
|
+
|
|
122
|
+
```ts
|
|
123
|
+
import { useHttpContext } from '@wooksjs/event-http'
|
|
124
|
+
|
|
125
|
+
interface TMyStore {
|
|
126
|
+
myFeature?: {
|
|
127
|
+
parsedToken?: { userId: string; role: string } | null
|
|
128
|
+
isAdmin?: boolean
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export function useMyFeature() {
|
|
133
|
+
const { store } = useHttpContext<TMyStore>()
|
|
134
|
+
const { init } = store('myFeature')
|
|
135
|
+
|
|
136
|
+
const parsedToken = () =>
|
|
137
|
+
init('parsedToken', () => {
|
|
138
|
+
const { authRawCredentials, isBearer } = useAuthorization()
|
|
139
|
+
if (!isBearer()) return null
|
|
140
|
+
return decodeToken(authRawCredentials()!)
|
|
141
|
+
})
|
|
142
|
+
|
|
143
|
+
const isAdmin = () =>
|
|
144
|
+
init('isAdmin', () => parsedToken()?.role === 'admin')
|
|
145
|
+
|
|
146
|
+
return { parsedToken, isAdmin }
|
|
147
|
+
}
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
For the full context store API and more composable patterns, see [event-core.md](event-core.md).
|
|
151
|
+
|
|
152
|
+
## Server Lifecycle
|
|
153
|
+
|
|
154
|
+
### `listen(...)`
|
|
155
|
+
|
|
156
|
+
Starts a built-in HTTP server:
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
await app.listen(3000)
|
|
160
|
+
await app.listen(3000, '0.0.0.0')
|
|
161
|
+
await app.listen({ port: 3000, host: '0.0.0.0' })
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### `close(server?)`
|
|
165
|
+
|
|
166
|
+
Stops the server:
|
|
167
|
+
|
|
168
|
+
```ts
|
|
169
|
+
await app.close()
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
### `getServer()`
|
|
173
|
+
|
|
174
|
+
Returns the underlying `http.Server` instance (only available after `listen()`):
|
|
175
|
+
|
|
176
|
+
```ts
|
|
177
|
+
const server = app.getServer()
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### `attachServer(server)`
|
|
181
|
+
|
|
182
|
+
Attaches an external server so `close()` can stop it:
|
|
183
|
+
|
|
184
|
+
```ts
|
|
185
|
+
const server = http.createServer(app.getServerCb())
|
|
186
|
+
app.attachServer(server)
|
|
187
|
+
server.listen(3000)
|
|
188
|
+
// later: await app.close()
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
### `getServerCb()`
|
|
192
|
+
|
|
193
|
+
Returns the raw `(req, res) => void` callback for use with any Node.js HTTP server:
|
|
194
|
+
|
|
195
|
+
```ts
|
|
196
|
+
const cb = app.getServerCb()
|
|
197
|
+
http.createServer(cb).listen(3000)
|
|
198
|
+
// or with https:
|
|
199
|
+
https.createServer(sslOpts, cb).listen(443)
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
## Sharing Router Between Adapters
|
|
203
|
+
|
|
204
|
+
Multiple adapters can share the same Wooks router:
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
import { Wooks } from 'wooks'
|
|
208
|
+
import { createHttpApp } from '@wooksjs/event-http'
|
|
209
|
+
|
|
210
|
+
const wooks = new Wooks()
|
|
211
|
+
const app1 = createHttpApp({}, wooks)
|
|
212
|
+
const app2 = createHttpApp({}, wooks) // shares the same routes
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Or share via another adapter instance:
|
|
216
|
+
|
|
217
|
+
```ts
|
|
218
|
+
const app1 = createHttpApp()
|
|
219
|
+
const app2 = createHttpApp({}, app1) // shares app1's router
|
|
220
|
+
```
|
|
221
|
+
|
|
222
|
+
## Logging
|
|
223
|
+
|
|
224
|
+
Get a scoped logger from the app:
|
|
225
|
+
|
|
226
|
+
```ts
|
|
227
|
+
const app = createHttpApp()
|
|
228
|
+
const logger = app.getLogger('[my-app]')
|
|
229
|
+
logger.log('App started')
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
Inside a handler, use the event-scoped logger:
|
|
233
|
+
|
|
234
|
+
```ts
|
|
235
|
+
import { useEventLogger } from '@wooksjs/event-core'
|
|
236
|
+
|
|
237
|
+
app.get('/process', () => {
|
|
238
|
+
const logger = useEventLogger('my-handler')
|
|
239
|
+
logger.log('Processing request') // tagged with event ID
|
|
240
|
+
return 'ok'
|
|
241
|
+
})
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
## Testing
|
|
245
|
+
|
|
246
|
+
`@wooksjs/event-http` exports a test utility for running composables outside a real server:
|
|
247
|
+
|
|
248
|
+
```ts
|
|
249
|
+
import { prepareTestHttpContext } from '@wooksjs/event-http'
|
|
250
|
+
|
|
251
|
+
const runInContext = prepareTestHttpContext({
|
|
252
|
+
url: '/users/42',
|
|
253
|
+
method: 'GET',
|
|
254
|
+
headers: { authorization: 'Bearer test-token' },
|
|
255
|
+
params: { id: '42' },
|
|
256
|
+
})
|
|
257
|
+
|
|
258
|
+
runInContext(() => {
|
|
259
|
+
const { get } = useRouteParams()
|
|
260
|
+
console.log(get('id')) // '42'
|
|
261
|
+
|
|
262
|
+
const { isBearer } = useAuthorization()
|
|
263
|
+
console.log(isBearer()) // true
|
|
264
|
+
})
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
### `prepareTestHttpContext(options)`
|
|
268
|
+
|
|
269
|
+
```ts
|
|
270
|
+
interface TTestHttpContext {
|
|
271
|
+
url: string
|
|
272
|
+
method?: string // default: 'GET'
|
|
273
|
+
headers?: Record<string, string>
|
|
274
|
+
params?: Record<string, string | string[]>
|
|
275
|
+
requestLimits?: TRequestLimits // app-level body limits for testing
|
|
276
|
+
cachedContext?: {
|
|
277
|
+
cookies?: Record<string, string | null>
|
|
278
|
+
authorization?: TAuthCache
|
|
279
|
+
body?: unknown // pre-parsed body
|
|
280
|
+
rawBody?: string | Buffer | Promise<Buffer>
|
|
281
|
+
raw?: Partial<THttpContextStore> // raw store sections
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
## Best Practices
|
|
287
|
+
|
|
288
|
+
- **Use `createHttpApp()` factory** — Don't instantiate `WooksHttp` directly unless you need to extend the class.
|
|
289
|
+
- **Use `getServerCb()` for custom servers** — This gives you full control over HTTPS, HTTP/2, or any custom server setup.
|
|
290
|
+
- **One composable per concern** — Split auth, validation, user-fetching into separate composables. Compose them in handlers.
|
|
291
|
+
- **Use `prepareTestHttpContext()` for unit testing composables** — Test composable logic without starting a server.
|
|
292
|
+
|
|
293
|
+
## Gotchas
|
|
294
|
+
|
|
295
|
+
- Composables must be called within a request handler (inside the async context). Calling them at module load time throws.
|
|
296
|
+
- `listen()` returns a promise — always `await` it or attach error handling.
|
|
297
|
+
- The framework auto-detects content type from your return value (string -> text/plain, object -> application/json). Override with `useSetHeaders().setContentType()`.
|