@wooksjs/event-http 0.7.7 → 0.7.9

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/dist/index.cjs CHANGED
@@ -1449,7 +1449,7 @@ function error_tl_default(ctx) {
1449
1449
  //#endregion
1450
1450
  //#region packages/event-http/src/response/wooks-http-response.ts
1451
1451
  let framework = {
1452
- version: "0.7.6",
1452
+ version: "0.7.8",
1453
1453
  poweredBy: "wooksjs",
1454
1454
  link: "https://wooks.moost.org/",
1455
1455
  image: "https://wooks.moost.org/wooks-full-logo.svg"
package/dist/index.mjs CHANGED
@@ -1426,7 +1426,7 @@ function error_tl_default(ctx) {
1426
1426
  //#endregion
1427
1427
  //#region packages/event-http/src/response/wooks-http-response.ts
1428
1428
  let framework = {
1429
- version: "0.7.6",
1429
+ version: "0.7.8",
1430
1430
  poweredBy: "wooksjs",
1431
1431
  link: "https://wooks.moost.org/",
1432
1432
  image: "https://wooks.moost.org/wooks-full-logo.svg"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wooksjs/event-http",
3
- "version": "0.7.7",
3
+ "version": "0.7.9",
4
4
  "description": "@wooksjs/event-http",
5
5
  "keywords": [
6
6
  "api",
@@ -25,13 +25,8 @@
25
25
  "url": "git+https://github.com/wooksjs/wooksjs.git",
26
26
  "directory": "packages/event-http"
27
27
  },
28
- "bin": {
29
- "wooksjs-event-http-skill": "scripts/setup-skills.js"
30
- },
31
28
  "files": [
32
- "dist",
33
- "skills",
34
- "scripts"
29
+ "dist"
35
30
  ],
36
31
  "main": "dist/index.cjs",
37
32
  "module": "dist/index.mjs",
@@ -47,17 +42,16 @@
47
42
  "devDependencies": {
48
43
  "typescript": "^5.9.3",
49
44
  "vitest": "^3.2.4",
50
- "@wooksjs/event-core": "^0.7.7",
51
- "wooks": "^0.7.7"
45
+ "@wooksjs/event-core": "^0.7.9",
46
+ "wooks": "^0.7.9"
52
47
  },
53
48
  "peerDependencies": {
54
49
  "@prostojs/logger": "^0.4.3",
55
50
  "@prostojs/router": "^0.3.3",
56
- "@wooksjs/event-core": "^0.7.7",
57
- "wooks": "^0.7.7"
51
+ "@wooksjs/event-core": "^0.7.9",
52
+ "wooks": "^0.7.9"
58
53
  },
59
54
  "scripts": {
60
- "build": "rolldown -c ../../rolldown.config.mjs",
61
- "setup-skills": "node scripts/setup-skills.js"
55
+ "build": "rolldown -c ../../rolldown.config.mjs"
62
56
  }
63
57
  }
@@ -1,77 +0,0 @@
1
- #!/usr/bin/env node
2
- /* prettier-ignore */
3
- 'use strict'
4
-
5
- const fs = require('fs')
6
- const path = require('path')
7
- const os = require('os')
8
-
9
- const SKILL_NAME = 'wooksjs-event-http'
10
- const SKILL_SRC = path.join(__dirname, '..', 'skills', SKILL_NAME)
11
-
12
- if (!fs.existsSync(SKILL_SRC)) {
13
- console.error(`No skills found at ${SKILL_SRC}`)
14
- console.error('Add your SKILL.md files to the skills/' + SKILL_NAME + '/ directory first.')
15
- process.exit(1)
16
- }
17
-
18
- const AGENTS = {
19
- 'Claude Code': { dir: '.claude/skills', global: path.join(os.homedir(), '.claude', 'skills') },
20
- 'Cursor': { dir: '.cursor/skills', global: path.join(os.homedir(), '.cursor', 'skills') },
21
- 'Windsurf': { dir: '.windsurf/skills', global: path.join(os.homedir(), '.windsurf', 'skills') },
22
- 'Codex': { dir: '.codex/skills', global: path.join(os.homedir(), '.codex', 'skills') },
23
- 'OpenCode': { dir: '.opencode/skills', global: path.join(os.homedir(), '.opencode', 'skills') },
24
- }
25
-
26
- const args = process.argv.slice(2)
27
- const isGlobal = args.includes('--global') || args.includes('-g')
28
- const isPostinstall = args.includes('--postinstall')
29
- let installed = 0, skipped = 0
30
- const installedDirs = []
31
-
32
- for (const [agentName, cfg] of Object.entries(AGENTS)) {
33
- const targetBase = isGlobal ? cfg.global : path.join(process.cwd(), cfg.dir)
34
- const agentRootDir = path.dirname(cfg.global) // Check if the agent has ever been installed globally
35
-
36
- // In postinstall mode: silently skip agents that aren't set up globally
37
- if (isPostinstall || isGlobal) {
38
- if (!fs.existsSync(agentRootDir)) { skipped++; continue }
39
- }
40
-
41
- const dest = path.join(targetBase, SKILL_NAME)
42
- try {
43
- fs.mkdirSync(dest, { recursive: true })
44
- fs.cpSync(SKILL_SRC, dest, { recursive: true })
45
- console.log(`[${SKILL_NAME}] installed to ${dest}`)
46
- installed++
47
- if (!isGlobal) installedDirs.push(cfg.dir + '/' + SKILL_NAME)
48
- } catch (err) {
49
- console.warn(`[${SKILL_NAME}] failed — ${err.message}`)
50
- }
51
- }
52
-
53
- // Add locally-installed skill dirs to .gitignore
54
- if (!isGlobal && installedDirs.length > 0) {
55
- const gitignorePath = path.join(process.cwd(), '.gitignore')
56
- let gitignoreContent = ''
57
- try { gitignoreContent = fs.readFileSync(gitignorePath, 'utf8') } catch {}
58
- const linesToAdd = installedDirs.filter(d => !gitignoreContent.includes(d))
59
- if (linesToAdd.length > 0) {
60
- const hasHeader = gitignoreContent.includes('# AI agent skills')
61
- const block = (gitignoreContent && !gitignoreContent.endsWith('\n') ? '\n' : '')
62
- + (hasHeader ? '' : '\n# AI agent skills (auto-generated by setup-skills)\n')
63
- + linesToAdd.join('\n') + '\n'
64
- fs.appendFileSync(gitignorePath, block)
65
- console.log(`[${SKILL_NAME}] added entries to .gitignore`)
66
- }
67
- }
68
-
69
- if (installed === 0 && isPostinstall) {
70
- // Silence is fine — no agents present, nothing to do
71
- } else if (installed === 0 && skipped === Object.keys(AGENTS).length) {
72
- console.log('No agent directories detected. Try --global or run without it for project-local install.')
73
- } else if (installed === 0) {
74
- console.log('Nothing installed. Run without --global to install project-locally.')
75
- } else {
76
- console.log(`Done! Restart your AI agent to pick up the "${SKILL_NAME}" skill.`)
77
- }
@@ -1,44 +0,0 @@
1
- ---
2
- name: wooksjs-event-http
3
- description: Use this skill when working with @wooksjs/event-http — to create HTTP servers with createHttpApp(), register route handlers with app.get()/post()/put()/patch()/delete(), read request data with useRequest()/useHeaders()/useCookies()/useUrlParams()/useAuthorization()/useAccept(), control responses with useResponse() and HttpResponse (status, headers, cookies, cache control, streaming), throw HTTP errors with HttpError, test handlers with prepareTestHttpContext(), or integrate with existing Node.js servers via getServerCb().
4
- ---
5
-
6
- # @wooksjs/event-http
7
-
8
- HTTP adapter for Wooks. Composable-based request handling where every piece of data is available on demand, typed, and cached per request. No middleware, no `req`/`res` parameters — just function calls.
9
-
10
- ## How to use this skill
11
-
12
- Read the domain file that matches the task. Do not load all files — only what you need.
13
-
14
- | Domain | File | Load when... |
15
- | -------------------- | -------------------------- | -------------------------------------------------------------------------------------- |
16
- | Core setup & routing | [core.md](core.md) | Creating an app, registering routes, starting a server, understanding the architecture |
17
- | Request composables | [request.md](request.md) | Reading request data: headers, cookies, query params, body, authorization, IP |
18
- | Response API | [response.md](response.md) | Setting status, headers, cookies, cache control, sending responses, error handling |
19
- | Testing | [testing.md](testing.md) | Writing tests for handlers and composables with `prepareTestHttpContext` |
20
-
21
- ## Quick reference
22
-
23
- ```ts
24
- import { createHttpApp } from '@wooksjs/event-http'
25
-
26
- // Composables (no arguments — context via AsyncLocalStorage)
27
- import {
28
- useRequest,
29
- useResponse,
30
- useHeaders,
31
- useCookies,
32
- useUrlParams,
33
- useAuthorization,
34
- useAccept,
35
- useRouteParams,
36
- useLogger,
37
- } from '@wooksjs/event-http'
38
-
39
- // Errors
40
- import { HttpError } from '@wooksjs/event-http'
41
-
42
- // Testing
43
- import { prepareTestHttpContext } from '@wooksjs/event-http'
44
- ```
@@ -1,152 +0,0 @@
1
- # Core setup & routing — @wooksjs/event-http
2
-
3
- > Creating an app, registering routes, server lifecycle, and architecture overview.
4
-
5
- ## Concepts
6
-
7
- `@wooksjs/event-http` wraps a Node.js HTTP server with Wooks' composable architecture. Each incoming request gets its own `EventContext` (from `@wooksjs/event-core`), and handlers are plain functions that return their response. All request data is accessed through composables — on demand, typed, cached.
8
-
9
- The adapter creates context, seeds it with the `IncomingMessage` and `HttpResponse`, looks up routes, and runs handlers. Handlers never receive `req`/`res` parameters.
10
-
11
- ## Installation
12
-
13
- ```bash
14
- pnpm add @wooksjs/event-http
15
- ```
16
-
17
- Peer dependencies: `@wooksjs/event-core`, `wooks`, `@prostojs/router`, `@prostojs/logger`.
18
-
19
- ## API Reference
20
-
21
- ### `createHttpApp(opts?): WooksHttp`
22
-
23
- Creates and returns a `WooksHttp` instance.
24
-
25
- ```ts
26
- import { createHttpApp } from '@wooksjs/event-http'
27
-
28
- const app = createHttpApp()
29
- app.get('/hello', () => 'Hello World!')
30
- app.listen(3000)
31
- ```
32
-
33
- **Options (`TWooksHttpOptions`):**
34
-
35
- | Option | Type | Description |
36
- | ---------------- | ------------------------------------ | ------------------------------------------------------------------------- |
37
- | `logger` | `TConsoleBase` | Custom logger instance |
38
- | `onNotFound` | `TWooksHandler` | Handler called when no route matches (default: 404 HttpError) |
39
- | `router` | router options | Custom router configuration |
40
- | `requestLimits` | `Omit<TRequestLimits, 'perRequest'>` | Default body size/timeout limits for all requests |
41
- | `responseClass` | `typeof WooksHttpResponse` | Custom response subclass (default: `WooksHttpResponse`) |
42
- | `defaultHeaders` | `Record<string, string \| string[]>` | Default headers applied to every response (e.g. from `securityHeaders()`) |
43
-
44
- ### Route registration
45
-
46
- ```ts
47
- app.get(path, handler)
48
- app.post(path, handler)
49
- app.put(path, handler)
50
- app.patch(path, handler)
51
- app.delete(path, handler)
52
- app.head(path, handler)
53
- app.options(path, handler)
54
- app.all(path, handler) // matches any HTTP method
55
- ```
56
-
57
- Handlers are plain functions. Return value becomes the response body:
58
-
59
- ```ts
60
- app.get('/users/:id', () => {
61
- const { params } = useRouteParams<{ id: string }>()
62
- return { id: params.id } // → 200 application/json
63
- })
64
-
65
- app.post('/users', async () => {
66
- const { parseBody } = useBody() // from @wooksjs/http-body
67
- const user = await parseBody<{ name: string }>()
68
- return { created: user.name } // → 201 application/json (POST auto-status)
69
- })
70
- ```
71
-
72
- ### Server lifecycle
73
-
74
- ```ts
75
- // Start server
76
- await app.listen(3000)
77
- await app.listen(3000, '0.0.0.0')
78
-
79
- // Use with existing server
80
- import http from 'http'
81
- const server = http.createServer(app.getServerCb())
82
- server.listen(3000)
83
- app.attachServer(server)
84
-
85
- // Stop server
86
- await app.close()
87
- ```
88
-
89
- ### `getServerCb()`
90
-
91
- Returns a `(req, res) => void` callback for use with any Node.js HTTP server:
92
-
93
- ```ts
94
- const cb = app.getServerCb()
95
- http.createServer(cb).listen(3000)
96
- // or with https, http2, etc.
97
- ```
98
-
99
- ## Routing
100
-
101
- Built on [`@prostojs/router`](https://github.com/prostojs/router). Supports:
102
-
103
- - Static routes: `/api/users`
104
- - Parametric routes: `/users/:id`
105
- - Wildcards: `/static/*`
106
- - Multiple wildcards: `/static/*/assets/*`
107
- - Regex constraints: `/api/time/:hours(\\d{2})h:minutes(\\d{2})m`
108
- - Regex wildcards: `/static/*(\\d+)`
109
-
110
- Route params accessed via `useRouteParams()`:
111
-
112
- ```ts
113
- app.get('/users/:id', () => {
114
- const { params } = useRouteParams<{ id: string }>()
115
- return { userId: params.id }
116
- })
117
- ```
118
-
119
- ## Auto-status
120
-
121
- When no explicit status is set, it's inferred from the HTTP method and response body:
122
-
123
- | Method | With body | Without body |
124
- | ------ | ------------ | -------------- |
125
- | GET | 200 OK | 204 No Content |
126
- | POST | 201 Created | 204 No Content |
127
- | PUT | 201 Created | 204 No Content |
128
- | PATCH | 202 Accepted | 204 No Content |
129
- | DELETE | 202 Accepted | 204 No Content |
130
-
131
- ## Handler chain
132
-
133
- Multiple handlers can be registered for the same route. If one throws, the next is tried:
134
-
135
- ```ts
136
- app.get('/resource', authHandler, mainHandler)
137
- ```
138
-
139
- If all handlers throw, the last error is sent as the response.
140
-
141
- ## Best Practices
142
-
143
- - Return values directly — the framework handles serialization and status codes
144
- - Use `HttpError` for error responses: `throw new HttpError(404)` or `throw new HttpError(400, 'Invalid input')`
145
- - Use `useResponse()` only when you need explicit control over headers, cookies, or status
146
- - For `getServerCb()`, call `attachServer(server)` if you want `close()` to work
147
-
148
- ## Gotchas
149
-
150
- - Handlers receive no arguments — all data comes from composables
151
- - `listen()` returns a Promise — `await` it or handle rejection
152
- - `getServerCb()` doesn't automatically attach the server — call `attachServer()` if needed
@@ -1,204 +0,0 @@
1
- # Request composables — @wooksjs/event-http
2
-
3
- > Reading request data: headers, cookies, query params, body, authorization, IP address.
4
-
5
- ## Concepts
6
-
7
- All request data is accessed through composables — functions that resolve context via `AsyncLocalStorage`. They take no arguments (optionally accept `ctx` for performance). Data is parsed lazily on first access and cached for the request lifetime.
8
-
9
- All composables are importable from `@wooksjs/event-http`.
10
-
11
- ## API Reference
12
-
13
- ### `useRequest(ctx?)`
14
-
15
- The primary request composable. Returns method, URL, headers, raw body, IP, and request limit controls.
16
-
17
- ```ts
18
- import { useRequest } from '@wooksjs/event-http'
19
-
20
- const { method, url, headers, rawBody, getIp, reqId } = useRequest()
21
- ```
22
-
23
- **Returned properties:**
24
-
25
- | Property | Type | Description |
26
- | ---------------- | ------------------------------------------------- | -------------------------------------------------- |
27
- | `raw` | `IncomingMessage` | Node.js raw request object |
28
- | `url` | `string` | Request URL |
29
- | `method` | `string` | HTTP method |
30
- | `headers` | `IncomingHttpHeaders` | Request headers |
31
- | `rawBody` | `() => Promise<Buffer>` | Lazy — reads and decompresses request body on call |
32
- | `reqId` | `() => string` | Lazy UUID per request |
33
- | `getIp(opts?)` | `(opts?: { trustProxy: boolean }) => string` | Client IP (with optional proxy trust) |
34
- | `getIpList()` | `() => { remoteIp: string; forwarded: string[] }` | All IPs (remote + X-Forwarded-For) |
35
- | `isCompressed()` | `() => boolean` | Whether the request body is compressed |
36
-
37
- **Request limits (per-request override):**
38
-
39
- ```ts
40
- const { setMaxCompressed, setMaxInflated, setMaxRatio, setReadTimeoutMs } = useRequest()
41
- setMaxCompressed(5 * 1024 * 1024) // 5 MB
42
- setReadTimeoutMs(30_000) // 30 seconds
43
- ```
44
-
45
- Default limits: `maxCompressed: 1MB`, `maxInflated: 10MB`, `maxRatio: 100`, `readTimeoutMs: 10s`.
46
-
47
- ### `useHeaders(ctx?): IncomingHttpHeaders`
48
-
49
- Returns request headers directly. Shorthand for `useRequest().headers`.
50
-
51
- ```ts
52
- import { useHeaders } from '@wooksjs/event-http'
53
-
54
- const { host, authorization, 'content-type': contentType } = useHeaders()
55
- ```
56
-
57
- ### `useCookies(ctx?)`
58
-
59
- Parses incoming request cookies lazily (per cookie name, via `cachedBy`).
60
-
61
- ```ts
62
- import { useCookies } from '@wooksjs/event-http'
63
-
64
- const { getCookie, raw } = useCookies()
65
- const session = getCookie('session_id') // parsed + cached
66
- const theme = getCookie('theme') // parsed + cached (different key)
67
- // raw = raw Cookie header string
68
- ```
69
-
70
- ### `useUrlParams(ctx?)`
71
-
72
- Provides access to URL query parameters.
73
-
74
- ```ts
75
- import { useUrlParams } from '@wooksjs/event-http'
76
-
77
- const { params, toJson, raw } = useUrlParams()
78
-
79
- // URLSearchParams API
80
- const page = params().get('page')
81
- const tags = params().getAll('tag')
82
-
83
- // As a plain object
84
- const query = toJson() // { page: '1', tag: ['a', 'b'] }
85
-
86
- // Raw query string
87
- const rawQuery = raw() // '?page=1&tag=a&tag=b'
88
- ```
89
-
90
- ### `useAuthorization(ctx?)`
91
-
92
- Parses the Authorization header (supports Basic and Bearer).
93
-
94
- ```ts
95
- import { useAuthorization } from '@wooksjs/event-http'
96
-
97
- const { authorization, type, credentials, is, basicCredentials } = useAuthorization()
98
-
99
- if (is('bearer')) {
100
- const token = credentials() // the raw token string
101
- }
102
-
103
- if (is('basic')) {
104
- const { username, password } = basicCredentials()!
105
- }
106
- ```
107
-
108
- **Returned properties:**
109
-
110
- | Property | Type | Description |
111
- | -------------------- | -------------------------------- | -------------------------------------------------------------- |
112
- | `authorization` | `string \| undefined` | Raw Authorization header value |
113
- | `type()` | `string \| null` | Auth scheme: `'Basic'`, `'Bearer'`, etc. |
114
- | `credentials()` | `string \| null` | Everything after the scheme |
115
- | `is(type)` | `boolean` | Check auth scheme: `'basic'`, `'bearer'`, or any custom scheme |
116
- | `basicCredentials()` | `{ username, password } \| null` | Decoded Basic credentials |
117
-
118
- ### `useAccept(ctx?)`
119
-
120
- Checks the request's `Accept` header for MIME type support. Uses short names (`'json'`, `'html'`, `'xml'`, `'text'`) or full MIME types.
121
-
122
- ```ts
123
- import { useAccept } from '@wooksjs/event-http'
124
-
125
- const { accept, has } = useAccept()
126
-
127
- if (has('json')) {
128
- /* ... */
129
- }
130
- if (has('image/png')) {
131
- /* ... */
132
- }
133
- ```
134
-
135
- ### `useRouteParams<T>(ctx?)`
136
-
137
- Route parameters from the URL. Re-exported from `@wooksjs/event-core`.
138
-
139
- ```ts
140
- import { useRouteParams } from '@wooksjs/event-http'
141
-
142
- // Given route: /users/:id/posts/:postId
143
- const { params, get } = useRouteParams<{ id: string; postId: string }>()
144
- params.id // '42'
145
- get('postId') // '7'
146
- ```
147
-
148
- ### `useLogger(ctx?): Logger`
149
-
150
- Logger from the current event context. Re-exported from `@wooksjs/event-core`.
151
-
152
- ```ts
153
- import { useLogger } from '@wooksjs/event-http'
154
-
155
- const log = useLogger()
156
- log.info('handling request')
157
- ```
158
-
159
- ## Common Patterns
160
-
161
- ### Pattern: Auth guard with early return
162
-
163
- ```ts
164
- app.post('/admin/action', async () => {
165
- const { is, credentials } = useAuthorization()
166
- if (!is('bearer')) throw new HttpError(401)
167
-
168
- const token = credentials()!
169
- const user = await verifyToken(token)
170
- if (!user.isAdmin) throw new HttpError(403)
171
-
172
- // Body is never parsed if auth fails
173
- const { parseBody } = useBody() // from @wooksjs/http-body
174
- return parseBody()
175
- })
176
- ```
177
-
178
- ### Pattern: Performance — resolve context once
179
-
180
- ```ts
181
- import { current } from '@wooksjs/event-core'
182
-
183
- app.get('/hot-path', () => {
184
- const ctx = current()
185
- const { method } = useRequest(ctx)
186
- const { getCookie } = useCookies(ctx)
187
- const { params } = useUrlParams(ctx)
188
- // 1 ALS lookup instead of 3
189
- })
190
- ```
191
-
192
- ## Best Practices
193
-
194
- - Composables are lazy — call them only when you need the data
195
- - Pass `ctx` explicitly in hot paths with multiple composable calls
196
- - Use `getCookie(name)` over parsing all cookies when you need only a few
197
- - `rawBody()` handles decompression (gzip, deflate, brotli) automatically with limits enforcement
198
-
199
- ## Gotchas
200
-
201
- - `rawBody()` returns a `Promise<Buffer>` — always `await` it
202
- - `rawBody()` consumes the stream — it's cached, so second call returns the same buffer
203
- - `getCookie()` returns `null` (not `undefined`) when a cookie doesn't exist
204
- - `useRequest()` limits setters use copy-on-write — they don't affect other requests
@@ -1,267 +0,0 @@
1
- # Response API — @wooksjs/event-http
2
-
3
- > Status, headers, cookies, cache control, error handling, and response sending.
4
-
5
- ## Concepts
6
-
7
- `useResponse()` returns an `HttpResponse` instance for the current request. All response operations — status, headers, cookies, cache control — are methods on this single object. Methods are chainable.
8
-
9
- The response is also controlled implicitly by return values: returning an object sends JSON, returning a string sends text, returning nothing sends 204.
10
-
11
- ## API Reference
12
-
13
- ### `useResponse(ctx?): HttpResponse`
14
-
15
- Returns the `HttpResponse` for the current request.
16
-
17
- ```ts
18
- import { useResponse } from '@wooksjs/event-http'
19
-
20
- const response = useResponse()
21
- response.setStatus(200).setHeader('x-custom', 'value')
22
- ```
23
-
24
- ### Status
25
-
26
- ```ts
27
- response.status = 201 // set via property
28
- response.setStatus(201) // set via method (chainable)
29
- const code = response.status // get current status
30
- ```
31
-
32
- If not set explicitly, status is inferred automatically (see core.md Auto-status).
33
-
34
- ### Headers
35
-
36
- ```ts
37
- response.setHeader('x-custom', 'value') // set a header (chainable)
38
- response.setHeaders({ 'x-one': '1', 'x-two': '2' }) // batch-set headers
39
- response.setHeader('x-multi', ['a', 'b']) // multi-value header
40
- response.getHeader('x-custom') // get header value
41
- response.removeHeader('x-custom') // remove a header (chainable)
42
- response.headers() // all headers as Record
43
- response.setContentType('application/xml') // shorthand for content-type
44
- response.getContentType() // get content-type
45
- response.enableCors('*') // set Access-Control-Allow-Origin (chainable)
46
- ```
47
-
48
- ### Cookies (outgoing)
49
-
50
- Set-Cookie headers are managed via `HttpResponse`:
51
-
52
- ```ts
53
- response.setCookie('session', 'abc123', {
54
- httpOnly: true,
55
- secure: true,
56
- sameSite: 'Strict',
57
- maxAge: 3600, // seconds (also accepts time strings)
58
- path: '/',
59
- domain: '.example.com',
60
- expires: new Date('2025-12-31'),
61
- })
62
-
63
- response.getCookie('session') // { value, attrs } or undefined
64
- response.removeCookie('session')
65
- response.clearCookies()
66
- response.setCookieRaw('name=value; Path=/; HttpOnly') // raw Set-Cookie string
67
- ```
68
-
69
- **`TCookieAttributes`:**
70
-
71
- | Attribute | Type | Description |
72
- | ---------- | ---------------------------------------- | ------------------ |
73
- | `expires` | `Date \| string \| number` | Expiration date |
74
- | `maxAge` | `number \| TTimeMultiString` | Max age in seconds |
75
- | `domain` | `string` | Cookie domain |
76
- | `path` | `string` | Cookie path |
77
- | `secure` | `boolean` | Secure flag |
78
- | `httpOnly` | `boolean` | HttpOnly flag |
79
- | `sameSite` | `boolean \| 'Lax' \| 'None' \| 'Strict'` | SameSite policy |
80
-
81
- ### Cache control
82
-
83
- ```ts
84
- response.setCacheControl({
85
- public: true,
86
- maxAge: 3600,
87
- sMaxage: 7200,
88
- noStore: false,
89
- noCache: false,
90
- mustRevalidate: true,
91
- })
92
-
93
- response.setAge(300) // Age header (seconds)
94
- response.setExpires(new Date('2025-12-31')) // Expires header
95
- response.setExpires('2025-12-31') // also accepts strings
96
- response.setPragmaNoCache() // Pragma: no-cache
97
- ```
98
-
99
- ### Body and content type inference
100
-
101
- Return values are automatically serialized:
102
-
103
- | Return type | Content-Type | Status |
104
- | -------------------------------- | --------------------- | -------------- |
105
- | `string` | `text/plain` | Auto |
106
- | `number` / `boolean` | `text/plain` | Auto |
107
- | `object` / `array` | `application/json` | Auto |
108
- | `Buffer` / `Uint8Array` | (none — set manually) | Auto |
109
- | `Readable` stream | (none — set manually) | Auto |
110
- | `Response` (fetch) | From fetch response | Auto |
111
- | `undefined` / `null` / no return | — | 204 No Content |
112
-
113
- ### Raw response access
114
-
115
- For escape hatches (SSE, WebSocket upgrades, etc.):
116
-
117
- ```ts
118
- // Take full control — framework won't send anything
119
- const res = response.getRawRes()
120
- res.writeHead(200, { 'content-type': 'text/event-stream' })
121
- res.write('data: hello\n\n')
122
-
123
- // Passthrough — you write to res, but framework still finalizes
124
- const res = response.getRawRes(true)
125
- ```
126
-
127
- ### `responded` property
128
-
129
- ```ts
130
- if (response.responded) {
131
- // Response already sent — don't try to send again
132
- }
133
- ```
134
-
135
- ## HttpError
136
-
137
- For error responses, throw `HttpError`:
138
-
139
- ```ts
140
- import { HttpError } from '@wooksjs/event-http'
141
-
142
- throw new HttpError(404) // 404 with default message
143
- throw new HttpError(400, 'Invalid email') // 400 with custom message
144
- throw new HttpError(422, { message: 'Validation failed', fields: ['email'] }) // structured body
145
- ```
146
-
147
- `HttpError` skips stack trace capture for performance (these are expected control-flow errors).
148
-
149
- **Error rendering (`WooksHttpResponse`):**
150
-
151
- The default response class renders errors based on the `Accept` header:
152
-
153
- - `application/json` → JSON `{ statusCode, message, error }`
154
- - `text/html` → Styled HTML error page with SVG icons
155
- - `text/plain` → Plain text error
156
-
157
- Override by providing a custom `responseClass` to `createHttpApp()`:
158
-
159
- ```ts
160
- class MyResponse extends WooksHttpResponse {
161
- protected renderError(data, ctx) {
162
- this._status = data.statusCode
163
- this._headers['content-type'] = 'application/json'
164
- this._body = JSON.stringify({ error: data.message })
165
- }
166
- }
167
-
168
- const app = createHttpApp({ responseClass: MyResponse })
169
- ```
170
-
171
- ### Default headers and security headers
172
-
173
- Pre-populate response headers for every request via `defaultHeaders`:
174
-
175
- ```ts
176
- import { createHttpApp, securityHeaders } from '@wooksjs/event-http'
177
-
178
- const app = createHttpApp({ defaultHeaders: securityHeaders() })
179
- ```
180
-
181
- `securityHeaders(opts?)` returns recommended HTTP security headers:
182
-
183
- | Header | Default |
184
- | ------------------------------ | --------------------------------------------------------------------------------- |
185
- | `content-security-policy` | `default-src 'self'; base-uri 'self'; form-action 'self'; frame-ancestors 'self'` |
186
- | `cross-origin-opener-policy` | `same-origin` |
187
- | `cross-origin-resource-policy` | `same-origin` |
188
- | `referrer-policy` | `no-referrer` |
189
- | `x-content-type-options` | `nosniff` |
190
- | `x-frame-options` | `SAMEORIGIN` |
191
-
192
- Options: `string` (override) or `false` (disable). `strictTransportSecurity` is opt-in only.
193
-
194
- ```ts
195
- securityHeaders({
196
- contentSecurityPolicy: false,
197
- referrerPolicy: 'strict-origin-when-cross-origin',
198
- strictTransportSecurity: 'max-age=31536000; includeSubDomains',
199
- })
200
- ```
201
-
202
- Per-endpoint: `response.setHeaders(securityHeaders({ ... }))`.
203
-
204
- ## Common Patterns
205
-
206
- ### Pattern: Full response control
207
-
208
- ```ts
209
- app.get('/data', () => {
210
- useResponse()
211
- .setStatus(200)
212
- .setHeader('x-request-id', useRequest().reqId())
213
- .setCookie('visited', 'true', { httpOnly: true })
214
- .setCacheControl({ public: true, maxAge: 3600 })
215
-
216
- return { data: 'hello' }
217
- })
218
- ```
219
-
220
- ### Pattern: Redirect
221
-
222
- ```ts
223
- app.get('/old-path', () => {
224
- useResponse().setStatus(301).setHeader('location', '/new-path')
225
- })
226
- ```
227
-
228
- ### Pattern: Streaming response
229
-
230
- ```ts
231
- import { createReadStream } from 'fs'
232
-
233
- app.get('/file', () => {
234
- useResponse().setContentType('application/octet-stream')
235
- return createReadStream('/path/to/file')
236
- })
237
- ```
238
-
239
- ### Pattern: Server-Sent Events
240
-
241
- ```ts
242
- app.get('/events', () => {
243
- const res = useResponse().getRawRes()
244
- res.writeHead(200, {
245
- 'content-type': 'text/event-stream',
246
- 'cache-control': 'no-cache',
247
- connection: 'keep-alive',
248
- })
249
- // Write events...
250
- return res // returning the raw response signals "already handled"
251
- })
252
- ```
253
-
254
- ## Best Practices
255
-
256
- - Return values for simple responses — only use `useResponse()` when you need headers, cookies, or explicit status
257
- - Use `HttpError` for all error responses — don't manually set error status and body
258
- - The chainable API means you can do `useResponse().setStatus(200).setHeader(...)` in one statement
259
- - For custom error rendering, subclass `WooksHttpResponse` and override `renderError()`
260
-
261
- ## Gotchas
262
-
263
- - Calling `send()` twice throws — check `response.responded` if unsure
264
- - `getRawRes()` without `passthrough` marks the response as "responded" — the framework won't touch it
265
- - `getRawRes(true)` (passthrough mode) lets you write headers/data while the framework still finalizes cookies and status
266
- - Cookie `maxAge` is in seconds, not milliseconds
267
- - `setContentType()` overwrites any previously set content-type
@@ -1,150 +0,0 @@
1
- # Testing — @wooksjs/event-http
2
-
3
- > Writing tests for handlers and composables with `prepareTestHttpContext`.
4
-
5
- ## Concepts
6
-
7
- `prepareTestHttpContext` creates a fully initialized HTTP event context for testing. It sets up an `EventContext` with `httpKind` seeds (fake `IncomingMessage`, `HttpResponse`, route params, and optional pre-seeded body), then returns a runner function that executes callbacks inside the context scope.
8
-
9
- ## API Reference
10
-
11
- ### `prepareTestHttpContext(options): (cb) => T`
12
-
13
- ```ts
14
- import { prepareTestHttpContext } from '@wooksjs/event-http'
15
- ```
16
-
17
- **Options (`TTestHttpContext`):**
18
-
19
- | Option | Type | Required | Description |
20
- | ---------------- | ------------------------------------ | -------- | ----------------------------------------------- |
21
- | `url` | `string` | Yes | Request URL (e.g. `/api/users?page=1`) |
22
- | `method` | `string` | No | HTTP method (default: `'GET'`) |
23
- | `headers` | `Record<string, string>` | No | Request headers |
24
- | `params` | `Record<string, string \| string[]>` | No | Pre-set route parameters |
25
- | `requestLimits` | `TRequestLimits` | No | Custom request limits |
26
- | `rawBody` | `string \| Buffer` | No | Pre-seed the raw body (skips stream reading) |
27
- | `defaultHeaders` | `Record<string, string \| string[]>` | No | Default headers to pre-populate on the response |
28
-
29
- **Returns:** `(cb: () => T) => T` — a runner function.
30
-
31
- ```ts
32
- const run = prepareTestHttpContext({
33
- url: '/users/42',
34
- method: 'GET',
35
- headers: { authorization: 'Bearer abc123' },
36
- params: { id: '42' },
37
- })
38
-
39
- run(() => {
40
- // All composables work here
41
- const { method } = useRequest()
42
- const { params } = useRouteParams()
43
- const { is } = useAuthorization()
44
- expect(method).toBe('GET')
45
- expect(params.id).toBe('42')
46
- expect(is('bearer')).toBe(true)
47
- })
48
- ```
49
-
50
- ## Common Patterns
51
-
52
- ### Pattern: Testing a custom composable
53
-
54
- ```ts
55
- import { describe, it, expect } from 'vitest'
56
- import { prepareTestHttpContext, useHeaders } from '@wooksjs/event-http'
57
- import { defineWook, cached } from '@wooksjs/event-core'
58
-
59
- const useApiKey = defineWook((ctx) => {
60
- const headers = useHeaders(ctx)
61
- return {
62
- apiKey: headers['x-api-key'] as string | undefined,
63
- isValid: () => headers['x-api-key'] === 'secret',
64
- }
65
- })
66
-
67
- describe('useApiKey', () => {
68
- it('extracts API key from headers', () => {
69
- const run = prepareTestHttpContext({
70
- url: '/api/data',
71
- headers: { 'x-api-key': 'secret' },
72
- })
73
-
74
- run(() => {
75
- const { apiKey, isValid } = useApiKey()
76
- expect(apiKey).toBe('secret')
77
- expect(isValid()).toBe(true)
78
- })
79
- })
80
- })
81
- ```
82
-
83
- ### Pattern: Testing body parsing
84
-
85
- ```ts
86
- import { useBody } from '@wooksjs/http-body'
87
-
88
- it('parses JSON body', async () => {
89
- const run = prepareTestHttpContext({
90
- url: '/api/users',
91
- method: 'POST',
92
- headers: { 'content-type': 'application/json' },
93
- rawBody: JSON.stringify({ name: 'Alice' }),
94
- })
95
-
96
- await run(async () => {
97
- const { parseBody } = useBody()
98
- const body = await parseBody<{ name: string }>()
99
- expect(body.name).toBe('Alice')
100
- })
101
- })
102
- ```
103
-
104
- ### Pattern: Testing response composable
105
-
106
- ```ts
107
- it('sets response headers', () => {
108
- const run = prepareTestHttpContext({ url: '/test' })
109
-
110
- run(() => {
111
- const response = useResponse()
112
- response.setHeader('x-custom', 'value')
113
- response.setCookie('session', 'abc', { httpOnly: true })
114
-
115
- expect(response.getHeader('x-custom')).toBe('value')
116
- expect(response.getCookie('session')?.value).toBe('abc')
117
- })
118
- })
119
- ```
120
-
121
- ### Pattern: Testing cookies
122
-
123
- ```ts
124
- it('reads cookies from request', () => {
125
- const run = prepareTestHttpContext({
126
- url: '/dashboard',
127
- headers: { cookie: 'session=abc123; theme=dark' },
128
- })
129
-
130
- run(() => {
131
- const { getCookie } = useCookies()
132
- expect(getCookie('session')).toBe('abc123')
133
- expect(getCookie('theme')).toBe('dark')
134
- expect(getCookie('missing')).toBeNull()
135
- })
136
- })
137
- ```
138
-
139
- ## Best Practices
140
-
141
- - Always use `prepareTestHttpContext` — don't manually construct `EventContext` for HTTP tests
142
- - Pre-seed `rawBody` for body parsing tests to avoid stream setup
143
- - Pre-seed `params` to test route parameter logic without a router
144
- - The runner function supports async callbacks — `await run(async () => { ... })`
145
-
146
- ## Gotchas
147
-
148
- - `prepareTestHttpContext` uses a real `IncomingMessage` and `ServerResponse` (from `new Socket({})`) — they're functional but not connected to a network
149
- - The `HttpResponse` in tests is the base `HttpResponse`, not `WooksHttpResponse` — error rendering won't do content negotiation
150
- - `rawBody` pre-seeding stores a resolved Promise — `useRequest().rawBody()` returns immediately