@wooksjs/event-http 0.6.1 → 0.6.3

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/package.json CHANGED
@@ -1,56 +1,64 @@
1
1
  {
2
2
  "name": "@wooksjs/event-http",
3
- "version": "0.6.1",
3
+ "version": "0.6.3",
4
4
  "description": "@wooksjs/event-http",
5
+ "keywords": [
6
+ "api",
7
+ "app",
8
+ "composables",
9
+ "framework",
10
+ "http",
11
+ "prostojs",
12
+ "rest",
13
+ "restful",
14
+ "web",
15
+ "wooks"
16
+ ],
17
+ "homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/event-http#readme",
18
+ "bugs": {
19
+ "url": "https://github.com/wooksjs/wooksjs/issues"
20
+ },
21
+ "license": "MIT",
22
+ "author": "Artem Maltsev",
23
+ "repository": {
24
+ "type": "git",
25
+ "url": "git+https://github.com/wooksjs/wooksjs.git",
26
+ "directory": "packages/event-http"
27
+ },
28
+ "bin": {
29
+ "setup-skills": "scripts/setup-skills.js"
30
+ },
31
+ "files": [
32
+ "dist",
33
+ "skills",
34
+ "scripts"
35
+ ],
5
36
  "main": "dist/index.cjs",
6
37
  "module": "dist/index.mjs",
7
38
  "types": "dist/index.d.ts",
8
- "files": [
9
- "dist"
10
- ],
11
39
  "exports": {
12
40
  "./package.json": "./package.json",
13
41
  ".": {
42
+ "types": "./dist/index.d.ts",
14
43
  "require": "./dist/index.cjs",
15
- "import": "./dist/index.mjs",
16
- "types": "./dist/index.d.ts"
44
+ "import": "./dist/index.mjs"
17
45
  }
18
46
  },
19
- "repository": {
20
- "type": "git",
21
- "url": "git+https://github.com/wooksjs/wooksjs.git",
22
- "directory": "packages/event-http"
23
- },
24
- "keywords": [
25
- "http",
26
- "wooks",
27
- "composables",
28
- "web",
29
- "framework",
30
- "app",
31
- "api",
32
- "rest",
33
- "restful",
34
- "prostojs"
35
- ],
36
- "author": "Artem Maltsev",
37
- "license": "MIT",
38
- "bugs": {
39
- "url": "https://github.com/wooksjs/wooksjs/issues"
47
+ "devDependencies": {
48
+ "typescript": "^5.9.3",
49
+ "vitest": "^3.2.4",
50
+ "@wooksjs/event-core": "^0.6.3",
51
+ "wooks": "^0.6.3"
40
52
  },
41
53
  "peerDependencies": {
42
- "@prostojs/router": "^0.2.1",
43
54
  "@prostojs/logger": "^0.4.3",
44
- "@wooksjs/event-core": "^0.6.1",
45
- "wooks": "^0.6.1"
46
- },
47
- "dependencies": {},
48
- "homepage": "https://github.com/wooksjs/wooksjs/tree/main/packages/event-http#readme",
49
- "devDependencies": {
50
- "typescript": "^5.8.3",
51
- "vitest": "^3.2.4"
55
+ "@prostojs/router": "^0.2.1",
56
+ "@wooksjs/event-core": "^0.6.3",
57
+ "wooks": "^0.6.3"
52
58
  },
53
59
  "scripts": {
54
- "build": "rolldown -c ../../rolldown.config.mjs"
60
+ "build": "rolldown -c ../../rolldown.config.mjs",
61
+ "postinstall": "node scripts/setup-skills.js",
62
+ "setup-skills": "node scripts/setup-skills.js"
55
63
  }
56
64
  }
@@ -0,0 +1,70 @@
1
+ #!/usr/bin/env node
2
+ // @ts-check
3
+ /**
4
+ * Copies the event-http skill files into the consuming project's
5
+ * agent-skills directory so that AI coding agents (Claude Code, Cursor,
6
+ * Windsurf, Codex, etc.) can discover and load them.
7
+ *
8
+ * Runs automatically on `postinstall` or manually via:
9
+ * npx @wooksjs/event-http setup-skills
10
+ */
11
+
12
+ const fs = require('fs')
13
+ const path = require('path')
14
+
15
+ const SKILL_NAME = 'wooksjs-event-http'
16
+
17
+ /* ── locate the project root (first dir above node_modules) ──────── */
18
+ function findProjectRoot() {
19
+ let dir = __dirname
20
+ while (dir !== path.dirname(dir)) {
21
+ dir = path.dirname(dir)
22
+ if (path.basename(dir) === 'node_modules') {
23
+ return path.dirname(dir)
24
+ }
25
+ }
26
+ return process.cwd()
27
+ }
28
+
29
+ const projectRoot = findProjectRoot()
30
+ const srcDir = path.join(__dirname, '..', 'skills', SKILL_NAME)
31
+ const agents = [
32
+ { dir: '.claude', file: 'SKILL.md' },
33
+ { dir: '.cursor', file: 'SKILL.md' },
34
+ { dir: '.windsurf', file: 'SKILL.md' },
35
+ { dir: '.codex', file: 'SKILL.md' },
36
+ ]
37
+
38
+ /* ── copy skill files ────────────────────────────────────────────── */
39
+ function copySkills() {
40
+ if (!fs.existsSync(srcDir)) {
41
+ console.log(`[${SKILL_NAME}] skills source not found, skipping.`)
42
+ return
43
+ }
44
+
45
+ const files = fs.readdirSync(srcDir)
46
+ let copied = 0
47
+
48
+ for (const agent of agents) {
49
+ const destDir = path.join(projectRoot, agent.dir, 'skills', SKILL_NAME)
50
+ fs.mkdirSync(destDir, { recursive: true })
51
+
52
+ for (const file of files) {
53
+ const src = path.join(srcDir, file)
54
+ const dest = path.join(destDir, file)
55
+ if (fs.statSync(src).isFile()) {
56
+ fs.copyFileSync(src, dest)
57
+ copied++
58
+ }
59
+ }
60
+ }
61
+
62
+ console.log(`[${SKILL_NAME}] copied ${files.length} skill files to ${agents.length} agent dirs (${copied} total).`)
63
+ }
64
+
65
+ try {
66
+ copySkills()
67
+ } catch (err) {
68
+ // Non-fatal — don't break installs
69
+ console.log(`[${SKILL_NAME}] skill setup skipped: ${err.message}`)
70
+ }
@@ -0,0 +1,37 @@
1
+ ---
2
+ name: wooksjs-event-http
3
+ description: Wooks HTTP framework — composable, lazy-evaluated HTTP server for Node.js. Load when building HTTP apps or REST APIs with wooks; defining routes or using the wooks router; using use-composables (useRequest, useResponse, useCookies, useHeaders, useBody, useProxy, useSearchParams, useRouteParams, useAuthorization, useSetHeaders, useSetCookies, useStatus, useAccept, useSetCacheControl); creating custom event context composables; working with @wooksjs/event-core context store (init, get, set, hook); serving static files; proxying requests; handling HTTP errors; setting status codes, content types, or cache control.
4
+ ---
5
+
6
+ # @wooksjs/event-http
7
+
8
+ A composable HTTP framework for Node.js built on async context (AsyncLocalStorage). Instead of middleware chains and mutated `req`/`res` objects, you call composable functions (`useRequest()`, `useCookies()`, etc.) anywhere in your handler — values are computed on demand and cached per request.
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
+ | Event context (core machinery) | [event-core.md](event-core.md) | Understanding the context store API (`init`/`get`/`set`/`hook`), creating custom composables, lazy evaluation and caching, building your own `use*()` functions |
17
+ | HTTP app setup | [core.md](core.md) | Creating an HTTP app, server lifecycle, `createHttpApp`, `getServerCb`, testing with `prepareTestHttpContext`, logging |
18
+ | Routing | [routing.md](routing.md) | Defining routes, route params (`:id`), wildcards (`*`), regex constraints (`:id(\\d+)`), optional params (`:tab?`), repeated params, path builders, HTTP method shortcuts, handler return values, router config |
19
+ | Request utilities | [request.md](request.md) | `useRequest`, `useHeaders`, `useCookies`, `useSearchParams`, `useAuthorization`, `useAccept`, `useEventId`, reading IP, body limits |
20
+ | Response & status | [response.md](response.md) | `useResponse`, `useStatus`, `useSetHeaders`, `useSetHeader`, `useSetCookies`, `useSetCookie`, `useSetCacheControl`, content type, status hooks, cookie hooks |
21
+ | Error handling | [error-handling.md](error-handling.md) | `HttpError`, throwing errors, custom error bodies, error rendering, guard patterns |
22
+ | Addons (body, static, proxy) | [addons.md](addons.md) | `useBody` (body parsing), `serveFile` (static files), `useProxy` (request proxying) |
23
+
24
+ ## Quick reference
25
+
26
+ ```ts
27
+ import { createHttpApp, useRouteParams } from '@wooksjs/event-http'
28
+
29
+ const app = createHttpApp()
30
+ app.get('/hello/:name', () => {
31
+ const { get } = useRouteParams<{ name: string }>()
32
+ return { greeting: `Hello ${get('name')}!` }
33
+ })
34
+ app.listen(3000)
35
+ ```
36
+
37
+ Key composables: `useRequest()`, `useResponse()`, `useRouteParams()`, `useHeaders()`, `useSetHeaders()`, `useCookies()`, `useSetCookies()`, `useSearchParams()`, `useAuthorization()`, `useAccept()`, `useSetCacheControl()`, `useStatus()`, `useBody()`, `useProxy()`.
@@ -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.