weifuwu 0.25.2 → 0.27.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/README.md +291 -2489
  2. package/ai/provider.ts +129 -0
  3. package/ai/stream.ts +63 -0
  4. package/{dist/cli.d.ts → cli.js} +1 -1
  5. package/cli.ts +55 -257
  6. package/core/cookie.ts +114 -0
  7. package/core/env.ts +142 -0
  8. package/core/logger.ts +72 -0
  9. package/core/router.ts +795 -0
  10. package/core/serve.ts +294 -0
  11. package/core/sse.ts +85 -0
  12. package/core/trace.ts +146 -0
  13. package/graphql.ts +267 -0
  14. package/hub.ts +133 -0
  15. package/index.ts +71 -0
  16. package/mailer.ts +81 -0
  17. package/middleware/compress.ts +103 -0
  18. package/middleware/cors.ts +81 -0
  19. package/middleware/csrf.ts +112 -0
  20. package/middleware/flash.ts +144 -0
  21. package/middleware/health.ts +44 -0
  22. package/middleware/helmet.ts +98 -0
  23. package/middleware/i18n.ts +175 -0
  24. package/middleware/rate-limit.ts +167 -0
  25. package/middleware/request-id.ts +60 -0
  26. package/middleware/static.ts +149 -0
  27. package/middleware/theme.ts +84 -0
  28. package/middleware/upload.ts +168 -0
  29. package/middleware/validate.ts +186 -0
  30. package/package.json +15 -36
  31. package/postgres/client.ts +132 -0
  32. package/postgres/index.ts +4 -0
  33. package/postgres/module.ts +37 -0
  34. package/postgres/schema/columns.ts +186 -0
  35. package/postgres/schema/index.ts +36 -0
  36. package/postgres/schema/sql.ts +39 -0
  37. package/postgres/schema/table.ts +548 -0
  38. package/postgres/schema/where.ts +99 -0
  39. package/postgres/types.ts +48 -0
  40. package/queue/cron.ts +90 -0
  41. package/queue/index.ts +654 -0
  42. package/queue/types.ts +60 -0
  43. package/redis/client.ts +24 -0
  44. package/{dist/redis/index.d.ts → redis/index.ts} +2 -2
  45. package/redis/types.ts +28 -0
  46. package/types.ts +78 -0
  47. package/cli/template/app.ts +0 -22
  48. package/cli/template/index.ts +0 -10
  49. package/cli/template/locales/en.json +0 -13
  50. package/cli/template/locales/zh-CN.json +0 -13
  51. package/cli/template/locales/zh-TW.json +0 -13
  52. package/cli/template/locales/zh.json +0 -13
  53. package/cli/template/ui/app/globals.css +0 -2
  54. package/cli/template/ui/app/layout.tsx +0 -15
  55. package/cli/template/ui/app/page.tsx +0 -124
  56. package/cli/template/ui/components/Greeting.tsx +0 -3
  57. package/dist/agent/client.d.ts +0 -2
  58. package/dist/agent/index.d.ts +0 -2
  59. package/dist/agent/rest.d.ts +0 -14
  60. package/dist/agent/run.d.ts +0 -19
  61. package/dist/agent/types.d.ts +0 -55
  62. package/dist/ai/provider.d.ts +0 -45
  63. package/dist/ai/utils.d.ts +0 -5
  64. package/dist/ai/workflow.d.ts +0 -17
  65. package/dist/ai-sdk.d.ts +0 -2
  66. package/dist/ai.d.ts +0 -13
  67. package/dist/analytics.d.ts +0 -45
  68. package/dist/auth.d.ts +0 -22
  69. package/dist/cache.d.ts +0 -74
  70. package/dist/cli.js +0 -302
  71. package/dist/client-locale.d.ts +0 -25
  72. package/dist/client-pref.d.ts +0 -3
  73. package/dist/client-router.d.ts +0 -300
  74. package/dist/client-state.d.ts +0 -22
  75. package/dist/client-theme.d.ts +0 -36
  76. package/dist/compile.d.ts +0 -15
  77. package/dist/compress.d.ts +0 -20
  78. package/dist/cookie.d.ts +0 -36
  79. package/dist/cors.d.ts +0 -25
  80. package/dist/cron-utils.d.ts +0 -73
  81. package/dist/csrf.d.ts +0 -47
  82. package/dist/deploy/config.d.ts +0 -2
  83. package/dist/deploy/gateway.d.ts +0 -2
  84. package/dist/deploy/index.d.ts +0 -4
  85. package/dist/deploy/manager.d.ts +0 -16
  86. package/dist/deploy/process.d.ts +0 -14
  87. package/dist/deploy/types.d.ts +0 -53
  88. package/dist/env.d.ts +0 -69
  89. package/dist/error-boundary.d.ts +0 -2
  90. package/dist/flash.d.ts +0 -90
  91. package/dist/fts.d.ts +0 -36
  92. package/dist/graphql.d.ts +0 -16
  93. package/dist/head.d.ts +0 -6
  94. package/dist/health.d.ts +0 -24
  95. package/dist/helmet.d.ts +0 -33
  96. package/dist/html-shell.d.ts +0 -1
  97. package/dist/hub.d.ts +0 -37
  98. package/dist/i18n.d.ts +0 -39
  99. package/dist/iii/client.d.ts +0 -2
  100. package/dist/iii/index.d.ts +0 -4
  101. package/dist/iii/register-worker.d.ts +0 -9
  102. package/dist/iii/rest.d.ts +0 -3
  103. package/dist/iii/stream.d.ts +0 -82
  104. package/dist/iii/types.d.ts +0 -121
  105. package/dist/iii/worker.d.ts +0 -2
  106. package/dist/iii/ws.d.ts +0 -22
  107. package/dist/index.d.ts +0 -101
  108. package/dist/index.js +0 -12752
  109. package/dist/kb/index.d.ts +0 -3
  110. package/dist/kb/types.d.ts +0 -72
  111. package/dist/layout.d.ts +0 -2
  112. package/dist/live.d.ts +0 -7
  113. package/dist/logdb/client.d.ts +0 -2
  114. package/dist/logdb/index.d.ts +0 -2
  115. package/dist/logdb/rest.d.ts +0 -5
  116. package/dist/logdb/types.d.ts +0 -27
  117. package/dist/logger.d.ts +0 -16
  118. package/dist/mailer.d.ts +0 -51
  119. package/dist/mcp.d.ts +0 -34
  120. package/dist/messager/agent.d.ts +0 -11
  121. package/dist/messager/client.d.ts +0 -2
  122. package/dist/messager/index.d.ts +0 -2
  123. package/dist/messager/rest.d.ts +0 -15
  124. package/dist/messager/types.d.ts +0 -57
  125. package/dist/messager/ws.d.ts +0 -14
  126. package/dist/module-server.d.ts +0 -9
  127. package/dist/not-found.d.ts +0 -2
  128. package/dist/notifier/client.d.ts +0 -2
  129. package/dist/notifier/index.d.ts +0 -2
  130. package/dist/notifier/types.d.ts +0 -105
  131. package/dist/opencode/client.d.ts +0 -2
  132. package/dist/opencode/index.d.ts +0 -2
  133. package/dist/opencode/permissions.d.ts +0 -5
  134. package/dist/opencode/prompt.d.ts +0 -8
  135. package/dist/opencode/rest.d.ts +0 -16
  136. package/dist/opencode/run.d.ts +0 -13
  137. package/dist/opencode/session.d.ts +0 -26
  138. package/dist/opencode/skills.d.ts +0 -4
  139. package/dist/opencode/tools/bash.d.ts +0 -6
  140. package/dist/opencode/tools/edit.d.ts +0 -19
  141. package/dist/opencode/tools/glob.d.ts +0 -9
  142. package/dist/opencode/tools/grep.d.ts +0 -17
  143. package/dist/opencode/tools/index.d.ts +0 -12
  144. package/dist/opencode/tools/question.d.ts +0 -5
  145. package/dist/opencode/tools/read.d.ts +0 -16
  146. package/dist/opencode/tools/skill.d.ts +0 -18
  147. package/dist/opencode/tools/web.d.ts +0 -18
  148. package/dist/opencode/tools/write.d.ts +0 -13
  149. package/dist/opencode/types.d.ts +0 -90
  150. package/dist/opencode/ws.d.ts +0 -21
  151. package/dist/permissions.d.ts +0 -51
  152. package/dist/postgres/client.d.ts +0 -4
  153. package/dist/postgres/index.d.ts +0 -4
  154. package/dist/postgres/module.d.ts +0 -17
  155. package/dist/postgres/schema/columns.d.ts +0 -99
  156. package/dist/postgres/schema/index.d.ts +0 -6
  157. package/dist/postgres/schema/sql.d.ts +0 -22
  158. package/dist/postgres/schema/table.d.ts +0 -141
  159. package/dist/postgres/schema/where.d.ts +0 -29
  160. package/dist/postgres/types.d.ts +0 -50
  161. package/dist/queue/index.d.ts +0 -2
  162. package/dist/queue/types.d.ts +0 -62
  163. package/dist/rate-limit.d.ts +0 -45
  164. package/dist/react.d.ts +0 -14
  165. package/dist/react.js +0 -751
  166. package/dist/redis/client.d.ts +0 -2
  167. package/dist/redis/types.d.ts +0 -18
  168. package/dist/request-id.d.ts +0 -40
  169. package/dist/router.d.ts +0 -73
  170. package/dist/s3.d.ts +0 -68
  171. package/dist/seo.d.ts +0 -104
  172. package/dist/serve.d.ts +0 -38
  173. package/dist/server-registry.d.ts +0 -10
  174. package/dist/session.d.ts +0 -117
  175. package/dist/sse.d.ts +0 -47
  176. package/dist/ssr-entries.d.ts +0 -4
  177. package/dist/ssr.d.ts +0 -11
  178. package/dist/static.d.ts +0 -23
  179. package/dist/stream.d.ts +0 -24
  180. package/dist/tailwind.d.ts +0 -15
  181. package/dist/tenant/client.d.ts +0 -2
  182. package/dist/tenant/graphql.d.ts +0 -3
  183. package/dist/tenant/index.d.ts +0 -2
  184. package/dist/tenant/rest.d.ts +0 -3
  185. package/dist/tenant/schema.d.ts +0 -5
  186. package/dist/tenant/types.d.ts +0 -48
  187. package/dist/tenant/utils.d.ts +0 -9
  188. package/dist/test-utils.d.ts +0 -194
  189. package/dist/theme.d.ts +0 -31
  190. package/dist/trace.d.ts +0 -95
  191. package/dist/tsx-context.d.ts +0 -32
  192. package/dist/types.d.ts +0 -47
  193. package/dist/upload.d.ts +0 -55
  194. package/dist/use-action.d.ts +0 -42
  195. package/dist/use-agent-stream.d.ts +0 -49
  196. package/dist/use-flash-message.d.ts +0 -17
  197. package/dist/use-websocket.d.ts +0 -42
  198. package/dist/user/client.d.ts +0 -30
  199. package/dist/user/index.d.ts +0 -2
  200. package/dist/user/oauth-login.d.ts +0 -21
  201. package/dist/user/oauth2.d.ts +0 -31
  202. package/dist/user/types.d.ts +0 -178
  203. package/dist/validate.d.ts +0 -32
  204. package/dist/vendor.d.ts +0 -7
  205. package/dist/webhook.d.ts +0 -79
  206. package/opencode/ui/app/globals.css +0 -1
  207. package/opencode/ui/app/layout.tsx +0 -13
  208. package/opencode/ui/app/page.tsx +0 -523
package/README.md CHANGED
@@ -1,2722 +1,524 @@
1
- ---
2
- name: weifuwu
3
- description: Web-standard HTTP framework for Node.js — (req, ctx) => Response
4
- ---
5
-
6
1
  # weifuwu
7
2
 
8
- **Web-standard HTTP framework for Node.js.** `(req, ctx) => Response` — no framework-specific objects.
9
-
10
- ## Quick Start
3
+ **Web-standard HTTP microframework for Node.js** `(req, ctx) => Response`
11
4
 
12
- ```ts
13
- import { serve } from 'weifuwu'
14
- serve((req, ctx) => new Response('Hello, World!'), { port: 3000 })
15
- ```
5
+ Pure Node.js, no build step. Native TypeScript via Node.js 24+.
16
6
 
17
- ```ts
18
- import { serve, Router, ssr } from 'weifuwu'
19
- const app = new Router()
20
- app.use('/', ssr({ dir: './ui' }))
21
- serve(app.handler(), { port: 3000, websocket: app.websocketHandler() })
22
7
  ```
23
-
24
- ```bash
25
- npx weifuwu init my-app && cd my-app && npm run dev
8
+ npm install weifuwu
26
9
  ```
27
10
 
28
- ## CLI
11
+ ---
29
12
 
30
- ### Typical Full App
13
+ ## Quick start
31
14
 
32
15
  ```ts
33
- import {
34
- serve,
35
- Router,
36
- postgres,
37
- session,
38
- user,
39
- aiProvider,
40
- ssr,
41
- flash,
42
- i18n,
43
- theme,
44
- logger,
45
- rateLimit,
46
- } from 'weifuwu'
16
+ import { serve, Router } from 'weifuwu'
47
17
 
48
18
  const app = new Router()
19
+ app.get('/', () => new Response('Hello world!'))
20
+ app.get('/api/ping', () => Response.json({ pong: true }))
49
21
 
50
- // 1. Observability (order matters run early)
51
- app.use(logger())
52
-
53
- // 2. UX middleware — single-line auto-registers middleware + routes
54
- app.use(theme())
55
- app.use(i18n({ default: 'zh', dir: './locales' }))
56
- app.use(flash())
57
-
58
- // 3. Database
59
- const pg = postgres()
60
- app.use(pg)
61
-
62
- // 4. Session & Auth
63
- app.use(session({ store: 'redis', redis: myRedis }))
64
- const auth = user({ pg, jwtSecret: process.env.JWT_SECRET })
65
- await auth.migrate()
66
- app.use(auth) // auto-registers middleware + /register, /login
67
- app.use('/auth', auth) // explicit path mounts for more control
68
-
69
- // 5. API protection
70
- app.use('/api', rateLimit({ max: 60, window: 60_000 }))
71
-
72
- // 6. AI
73
- app.use(aiProvider()) // ctx.ai
74
-
75
- // 7. SSR
76
- app.use('/', ssr({ dir: './ui' }))
77
-
78
- // 8. REST API
79
- app.get('/api/ping', () => Response.json({ ok: true }))
80
- app.post('/api/chat', async (req, ctx) => {
81
- const { prompt } = await req.json()
82
- const result = await ctx.ai.generateText({ prompt })
83
- return Response.json(result)
84
- })
85
-
86
- // 9. Start
87
- const server = serve(app.handler(), { port: 3000 })
88
- ```
89
-
90
- ```bash
91
- npx weifuwu init my-app # Full project (SSR + i18n + theme + WS demo)
92
- npx weifuwu init my-api --minimal # Minimal HTTP project (2 files)
93
- npx weifuwu init my-api --skip-install # Skip npm install
94
- npx weifuwu dev # Start dev server (auto-detect index.ts)
95
- npx weifuwu generate module my-mod # Scaffold middleware module + test
96
- npx weifuwu version # Print version
22
+ serve(app.handler(), { port: 3000 })
97
23
  ```
98
24
 
99
- ---
100
-
101
- ## Core Concepts
102
-
103
- ### serve()
104
-
105
- ```ts
106
- const server = serve(handler, { port: 3000 })
107
- await server.ready
108
- ```
25
+ ## Core concepts
109
26
 
110
- | Option | Type | Default | Description |
111
- | ------------------ | ------------------ | ----------- | ------------------------------ |
112
- | `port` | `number` | `0` | Listen port |
113
- | `hostname` | `string` | `'0.0.0.0'` | Listen address |
114
- | `signal` | `AbortSignal` | — | Shutdown on abort |
115
- | `websocket` | `WsUpgradeHandler` | — | WebSocket upgrade handler |
116
- | `maxBodySize` | `number` | `10MB` | Max body bytes (0 = unlimited) |
117
- | `timeout` | `number` | `30_000` | Socket inactivity timeout (ms) |
118
- | `keepAliveTimeout` | `number` | `5_000` | Keep-Alive idle timeout (ms) |
119
- | `headersTimeout` | `number` | `6_000` | Headers read timeout (ms) |
120
- | `shutdown` | `boolean` | `true` | Auto SIGTERM/SIGINT |
27
+ ### Handler
121
28
 
122
29
  ```ts
123
- interface Server {
124
- stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
125
- readonly port: number
126
- readonly hostname: string
127
- ready: Promise<void>
128
- }
129
- const { server, url } = await createTestServer(handler)
30
+ type Handler = (req: Request, ctx: Context) => Response | Promise<Response>
130
31
  ```
131
32
 
132
- `server.stop()` performs a graceful shutdown: stops accepting new connections,
133
- closes idle keep-alive sockets, then waits for in-flight requests to complete.
134
- If they don't finish within `timeoutMs` (default 10 seconds), remaining connections
135
- are forcibly closed. SIGTERM/SIGINT use the same graceful pattern.
33
+ Standard `Request` in, standard `Response` out. No framework-specific request/response objects.
136
34
 
137
35
  ### Router
138
36
 
139
37
  ```ts
140
38
  const app = new Router()
141
- app.get('/hello/:name', (req, ctx) => Response.json({ message: `Hello, ${ctx.params.name}!` }))
142
- app.post('/data', async (req, ctx) => {
143
- const body = await req.json()
144
- return Response.json(body, { status: 201 })
145
- })
146
- app.use('/admin', authMW) // path-scoped middleware
147
- app.use('/admin', adminRouter) // sub-router (flattened into parent trie)
148
- app.ws('/echo', {
149
- open(ws, ctx) {
150
- ctx.ws.json({ type: 'connected' })
151
- },
152
- message(ws, ctx, data) {
153
- ctx.ws.json({ echo: data.toString() })
154
- },
155
- })
156
- app.ws('/chat', {
157
- open(ws, ctx) {
158
- ctx.ws.join('room')
159
- },
160
- message(ws, ctx, data) {
161
- ctx.ws.sendRoom('room', JSON.parse(data.toString()))
162
- },
163
- })
164
- app.onError((err, req, ctx) => Response.json({ error: err.message }, { status: 500 }))
165
-
166
- // Debug: list all registered routes
167
- console.log(app.routes())
168
- // [ 'GET /hello/:name', 'POST /data', 'WS /echo', 'WS /chat' ]
169
-
170
- // Cross-process WebSocket broadcast (Redis)
171
- import { createHub } from 'weifuwu'
172
- app.wsHub(createHub({ redis: redis() }))
173
-
174
- const handler = app.handler()
175
- const wsHandler = app.websocketHandler()
176
- serve(handler, { port: 3000, websocket: wsHandler })
177
- ```
178
-
179
- | Pattern | Example | Match |
180
- | -------- | ------------ | ----------------------------- |
181
- | Static | `/about` | exact |
182
- | Param | `/users/:id` | `/users/42` → `ctx.params.id` |
183
- | Wildcard | `/static/*` | `/static/js/app.js` |
184
-
185
- Query params → `ctx.query`.
186
-
187
- ### Request lifecycle
188
-
189
- ```
190
- Request → serve() → app.handler() → global middleware × N → path middleware × N → route handler → Response
191
-
192
- mountPath set by sub-router
39
+ app.get('/users', handler)
40
+ app.post('/users', handler)
41
+ app.get('/users/:id', handler)
42
+ app.ws('/chat', { message(ws, ctx, data) { ... } })
193
43
  ```
194
44
 
195
- 1. `serve()` receives HTTP request
196
- 2. `app.handler()` creates `ctx = { params, query }` and routes to the matching trie node
197
- 3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
198
- 4. **Path‑scoped middleware** runs for matching paths (e.g. `app.use('/admin', authMW)`)
199
- 5. **Route‑level middleware** runs (e.g. `app.get('/admin', validate(...), handler)`)
200
- 6. **Route handler** returns `Response` — middleware chain unwinds
201
-
202
- Sub-routers (`app.use('/admin', adminRouter)`) are **flattened** into the parent trie. The sub-router's global middleware merges with the parent's. `ctx.mountPath` is set when entering a sub-router, allowing each module to derive its own paths.
45
+ Returns a `handler()` function compatible with `serve()`.
203
46
 
204
47
  ### Middleware
205
48
 
206
49
  ```ts
207
50
  type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>
208
- app.use(mw) // global
209
- app.use('/admin', mw) // path-scoped
210
- app.get('/admin', mw, handler) // route-level
211
- ```
212
-
213
- ### Middleware Dependency Checking
214
-
215
- Middleware factories can declare what ctx fields they inject and depend on via
216
- `__meta`. The Router warns at registration time if a dependency is unsatisfied.
217
-
218
- ```ts
219
- // postgres() declares: __meta = { injects: ['sql'], depends: [] }
220
- // session() declares: __meta = { injects: ['session'], depends: [] }
221
- // user() declares: __meta = { injects: ['user'], depends: ['sql', 'session'] }
222
-
223
- const app = new Router()
224
- app.use(user()) // ⚠️ Warns: depends on 'sql' and 'session' but they aren't registered
225
- // → "[weifuwu] Middleware at "global" depends on ctx.sql but it hasn't been registered yet."
226
- // → "Register the provider before this middleware: app.use(sql())"
227
-
228
- // Correct order:
229
- app.use(postgres())
230
- app.use(session())
231
- app.use(user())
232
51
  ```
233
52
 
234
- To add `__meta` to your own middleware:
235
-
236
- ```ts
237
- function myMiddleware() {
238
- const mw = async (req, ctx, next) => {
239
- ctx.myField = await setup()
240
- return next(req, ctx)
241
- }
242
- mw.__meta = { injects: ['myField'], depends: ['sql'] }
243
- return mw
244
- }
245
- ```
246
-
247
- The check is purely advisory — warnings go to `console.warn`, no errors are thrown. Built-in
248
- middleware (`postgres`, `redis`, `session`, `aiProvider`, `rateLimit`) all have `__meta` pre-attached.
249
-
250
- _New in v0.25._
251
-
252
- ### Context
253
-
254
- The `ctx` object accumulates properties as it passes through the middleware chain. Below are all documented properties:
255
-
256
- | Property | Set by | Type | Description |
257
- | ------------- | -------------------------------- | ------------------------- | ------------------------------------ |
258
- | `params` | Router | `Record<string, string>` | URL path parameters |
259
- | `query` | Router | `Record<string, string>` | URL query parameters |
260
- | `mountPath` | Router | `string` | Current sub-router mount prefix |
261
- | `env` | `loadEnv()` | `Record<string, string>` | Public env vars (`WEIFUWU_PUBLIC_*`) |
262
- | `csrf.token` | `csrf()` | `string` | CSRF token (namespace) |
263
- | `requestId` | `requestId()` | `string` | Request ID |
264
- | `session` | `session()` | `Session` | Session data object |
265
- | `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
266
- | `redis` | `redis()` | `Redis` | Redis client |
267
- | `ai` | `aiProvider()` | `AIProvider` | AI model & embedding |
268
- | `queue` | `queue()` | `Queue` | Job queue |
269
- | `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
270
- | `permissions` | `permissions()` | `{ roles, permissions }` | RBAC roles & permissions sets |
271
- | `theme` | `theme()` | `{ value, set }` | Current theme + switcher |
272
- | `i18n` | `i18n()` | `{ locale, t, set }` | Locale, translation, switcher |
273
- | `flash` | `flash()` | `{ value, set }` | Flash message + setter |
274
- | `tailwind` | `tailwindContext()` | `{ css, url }` | Compiled Tailwind CSS |
275
- | `tenant` | `tenant()` | `TenantContext` | Current tenant info |
276
- | `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
277
- | `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
278
- | `notifier` | `notifier()` | `Notifier` | Multi-channel notification system |
279
- | `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
280
- | `mountPath` | `Router` | `string` | Sub-router mount path |
281
- | `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
282
-
283
- ### Type-Safe Context
284
-
285
- Middleware-injected properties are **automatically typed** through chained `use()` calls:
53
+ Middleware enriches `ctx` with additional properties:
286
54
 
287
55
  ```ts
288
- const app = new Router()
289
- .use(csrf()) // → Router<Context & { csrf: { token: string } }>
290
- .use(requestId()) // → Router<Context & { csrf: ..., requestId }>
291
- .use(postgres()) // → Router<Context & { csrf: ..., requestId, sql }>
292
-
293
- app.get('/me', (_req, ctx) => {
294
- ctx.csrf.token // ✅ string (IDE autocomplete)
295
- ctx.requestId // ✅ string
296
- ctx.sql`SELECT 1` // ✅ Sql<{}>
297
- })
56
+ app.use(postgres()) // ctx.sql
57
+ app.use(redis()) // → ctx.redis
58
+ app.use(aiProvider()) // → ctx.ai
59
+ app.use(queue()) // → ctx.queue
60
+ app.use(cors())
61
+ app.use(rateLimit({ window: 60 }))
298
62
  ```
299
63
 
300
- Each module exports an `XxxInjected` type (e.g. `PostgresInjected`, `UserInjected`) for composing custom context types. `Context` is an interface — modules augment it via `declare module` for ambient compatibility.
301
-
302
64
  ---
303
65
 
304
- ## Module Patterns
305
-
306
- All modules follow one of **4 patterns** — learn these and you know every module.
307
-
308
- | Pattern | How to mount | Example |
309
- | ------- | ---------------------------------------- | ------------------------------------------------------ |
310
- | `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
311
- | `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
312
- | `[γ]` | Import and call directly | `mailer()`, `fts`, `cron-utils` |
313
- | `[δ]` | `import { useXxx } from 'weifuwu/react'` | `useTheme()`, `useLocale()`, `useWebsocket()` |
314
-
315
- ### Pattern α — Middleware
316
-
317
- ```ts
318
- app.use(compress()) // basic
319
- const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
320
- app.use(pg)
321
- app.use(rateLimit({ max: 100 })) // with .close()
322
- ```
66
+ ## Public API
323
67
 
324
- ### Pattern β — Router
68
+ ### serve
325
69
 
326
70
  ```ts
327
- app.use('/health', health()) // with path
328
- app.use('/graphql', graphql(handler))
329
- app.use('/logs', logdb({ pg })) // with .log(), .migrate()
330
- app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
331
- app.ws('/ws', messager({ pg }).wsHandler())
332
- ```
333
-
334
- β modules that need **separate middleware** use `.middleware()`. Most can auto-register both middleware and routes in one call:
71
+ import { serve } from 'weifuwu'
335
72
 
336
- ```ts
337
- app.use(theme()) // auto: middleware + /__theme/:value
338
- app.use(i18n({ dir: './locales' })) // auto: middleware + /__lang/:locale
339
- app.use(analytics({ pg })) // auto: middleware + /__analytics
340
- app.use(auth) // auto: middleware + /register, /login (user())
341
-
342
- // Explicit form when more control is needed:
343
- const a = analytics()
344
- app.use(a.middleware()) // tracking only
345
- app.use('/', a) // dashboard at custom path
73
+ const server = serve(handler, {
74
+ port: 3000, // default: 3000
75
+ websocket: wsHandler, // optional WebSocket handler from Router
76
+ shutdown: true, // graceful shutdown on SIGTERM/SIGINT (default: true)
77
+ maxBody: 1024 * 1024, // max request body size (default: 1MB)
78
+ })
346
79
  ```
347
80
 
348
- ### Pattern γ Standalone
81
+ Returns `{ close(): Promise<void> }`.
349
82
 
350
- Modules that don't intercept requests or serve routes. Import and use directly.
83
+ ### Router
351
84
 
352
85
  ```ts
353
- import { mailer, cronNext, fts } from 'weifuwu'
86
+ import { Router } from 'weifuwu'
354
87
 
355
- const email = mailer({ transport: 'smtp://...', from: 'noreply@example.com' })
356
- await email.send({ to: 'user@test.com', subject: 'Hello', text: 'Body' })
88
+ const r = new Router()
357
89
 
358
- const next = cronNext('0 9 * * 1-5') // next weekday at 09:00
359
- ```
360
-
361
- ### Pattern δ — Client-side
362
-
363
- React hooks that self-register via `addInterceptor()`. Import to enable.
364
-
365
- ```tsx
366
- import { useTheme, useLocale, useWebsocket } from 'weifuwu/react'
90
+ // HTTP methods
91
+ r.get(path, handler)
92
+ r.post(path, handler)
93
+ r.put(path, handler)
94
+ r.patch(path, handler)
95
+ r.delete(path, handler)
96
+ r.head(path, handler)
97
+ r.options(path, handler)
367
98
 
368
- function ThemeToggle() {
369
- const { theme, setTheme } = useTheme()
370
- return <button onClick={() => setTheme('dark')}>Dark</button>
371
- }
372
- ```
99
+ // Middleware (applied to all routes)
100
+ r.use(middleware) // global middleware
101
+ r.use('/prefix', middleware) // scoped to prefix
373
102
 
374
- ---
103
+ // WebSocket
104
+ r.ws(path, {
105
+ open(ws, ctx) { ... },
106
+ message(ws, ctx, data) { ... },
107
+ close(ws, ctx) { ... },
108
+ })
375
109
 
376
- ## Module Dependency Map
377
-
378
- ```mermaid
379
- graph TD
380
- serve --> Router
381
- Router --> postgres
382
- Router --> redis
383
- Router --> aiProvider
384
-
385
- subgraph "DB-Dependent Modules"
386
- user --> postgres
387
- session --> postgres
388
- session -.-> redis
389
- queue --> postgres
390
- queue -.-> redis
391
- permissions --> postgres
392
- analytics --> postgres
393
- logdb --> postgres
394
- tenant --> postgres
395
- messager --> postgres
396
- messager -.-> redis
397
- agent --> postgres
398
- kb --> postgres
399
- iii --> postgres
400
- iii -.-> redis
401
- end
402
-
403
- subgraph "AI-Dependent Modules"
404
- agent --> aiProvider
405
- kb --> aiProvider
406
- aiStream --> aiProvider
407
- opencode --> aiProvider
408
- runWorkflow --> aiProvider
409
- end
110
+ // Compose
111
+ r.handler() // → (req, ctx) => Response (for serve())
112
+ r.websocketHandler() // → WebSocket upgrade handler
410
113
  ```
411
114
 
412
- ## Quick Module Selection
413
-
414
- | What do you want to do? | Module | Pattern |
415
- | -------------------------------- | ----------------------------------------------- | ---------------------- |
416
- | **User registration / login** | `user()` | β |
417
- | **Simple token/header auth** | `auth()` | α |
418
- | **JWT verification** | `user().middleware()` | α |
419
- | **Role-based access control** | `permissions()` | α |
420
- | **AI chat / generate / stream** | `ctx.ai.generateText()` / `ctx.ai.streamText()` | α (via `aiProvider()`) |
421
- | **AI agent with knowledge** | `agent()` + `knowledgeBase()` | β |
422
- | **Send email** | `mailer()` | γ |
423
- | **File upload** | `upload()` | α |
424
- | **Object storage (S3/MinIO)** | `s3()` | α |
425
- | **Rate limiting** | `rateLimit()` | α |
426
- | **Response caching** | `cache()` | α |
427
- | **Periodic / delayed jobs** | `queue()` | α |
428
- | **Page view analytics** | `analytics()` | β |
429
- | **Structured logging** | `logdb()` | β |
430
- | **Real-time chat / messager** | `messager()` | β |
431
- | **Full-text search** | `fts` | γ |
432
- | **Theme switching** | `theme()` | α |
433
- | **i18n / localization** | `i18n()` | α |
434
- | **Flash messages** | `flash()` | α |
435
- | **Server-Sent Events** | `createSSEStream()` | γ |
436
- | **GraphQL endpoint** | `graphql()` | β |
437
- | **Webhook receiver** | `webhook()` | β |
438
- | **SSR with React** | `ssr()` | β |
439
- | **Health check** | `health()` | β |
440
- | **SEO (robots.txt, sitemap)** | `seo()` | β |
441
- | **Multi-process deploy** | `deploy()` | γ |
442
- | **Distributed functions (iii)** | `iii()` | β |
443
- | **Multi-tenant BaaS** | `tenant()` | β |
444
- | **Client-side routing** | `useNavigate()`, `<Link>` | δ |
445
- | **WebSocket in React** | `useWebsocket()` | δ |
446
- | **Compression (brotli/gzip)** | `compress()` | α |
447
- | **Security headers (CSP, HSTS)** | `helmet()` | α |
448
- | **CORS** | `cors()` | α |
449
- | **CSRF protection** | `csrf()` | α |
450
- | **Request ID tracing** | `requestId()` | α |
451
- | **Environment variables** | `env()` / `loadEnv()` | α |
452
- | **Static file serving** | `serveStatic()` | α |
453
- | **Object storage (S3/MinIO)** | `s3()` | α |
454
- | **Send email** | `mailer()` | γ |
455
- | **Scheduled / cron tasks** | `cron-utils` (`cronNext()`) | γ |
456
- | **Server-Sent Events** | `createSSEStream()` | γ |
457
- | **Multi-process deploy** | `deploy()` | γ |
458
- | **Distributed functions (iii)** | `iii()` | β |
459
- | **Webhook receiver** | `webhook()` | β |
460
- | **MCP tool integration** | `mcpClient()` | γ |
461
- | **Notifications** | `notifier()` | α |
462
- | **API Key management** | `user({ apiKeys: true })` | β |
463
- | **WebSocket testing** | `testApp().wsReq()` | — |
464
- | **Social login (OAuth)** | `user({ oauthLogin })` | β |
465
- | **Database migrations** | `pg.migrate()` | — |
466
-
467
- ---
468
-
469
- ## Request Tracing & Logging
470
-
471
- Every request gets a **trace ID** via `AsyncLocalStorage`, injected into responses as `X-Trace-Id`. W3C `traceparent` headers are forwarded.
115
+ ### Context
472
116
 
473
117
  ```ts
474
- import { currentTraceId } from 'weifuwu'
475
-
476
- app.get('/api', (req, ctx) => {
477
- console.log('Handling request', currentTraceId()) // f240a3f3-60e2-...
478
- })
479
- ```
480
-
481
- **Structured logging** — `logger({ format: 'json' })` outputs JSON to stderr with `traceId`, `timestamp`, `elapsed_ms`:
482
-
483
- ```json
484
- {
485
- "level": "info",
486
- "message": "request",
487
- "method": "GET",
488
- "path": "/api/users",
489
- "status": 200,
490
- "elapsed_ms": 42,
491
- "traceId": "f240a3f3-...",
492
- "timestamp": "2025-01-15T10:30:00.000Z"
118
+ interface Context {
119
+ params: Record<string, string> // URL parameters
120
+ query: Record<string, string> // query string
121
+ mountPath?: string // prefix path if mounted under Router.use()
122
+ [key: string]: unknown // middleware-injected fields
493
123
  }
494
124
  ```
495
125
 
496
- Default format is `'short'` (human-readable). `'combined'` includes query strings.
126
+ ### Middleware modules
497
127
 
498
- ---
128
+ #### postgres()
499
129
 
500
- ## AI Observability
130
+ ```ts
131
+ import { postgres } from 'weifuwu'
501
132
 
502
- Agent runs are **automatically logged** to `_agent_runs`. Dashboard endpoints provide analytics:
133
+ app.use(postgres({ connection: process.env.DATABASE_URL }))
134
+ // ctx.sql → SqlClient
503
135
 
136
+ const rows = await ctx.sql`SELECT * FROM users WHERE id = ${id}`
504
137
  ```
505
- GET /agents/:id/runs?days=7 → [{ input, output, tokens_in, tokens_out, elapsed_ms, status, trace_id, ... }]
506
- GET /agents/:id/runs/summary?days=7 → { total, success, error, success_rate, tokens_in, tokens_out, avg_elapsed_ms, p95_elapsed_ms }
507
- GET /opencode/sessions/:id/usage → { message_count, tokens_in, tokens_out, tokens_total }
508
- ```
509
-
510
- Non-streaming runs log full token data; streaming runs log `status: 'stream'`.
511
-
512
- ---
513
-
514
- ## Agent ↔ Messager Streaming
515
138
 
516
- Agent replies in messager channels now stream **token-by-token** via WebSocket:
139
+ Includes table builder and migrations:
517
140
 
518
141
  ```ts
519
- // Backend automatic when agents are attached to messager
520
- const msg = messager({ pg, agents: agent({ pg, model }) })
521
- app.ws('/ws', msg.wsHandler())
522
- // Agent replies stream to: hub.broadcast({ type: 'agent_stream', data: { token, full } })
142
+ const { sql, migrate } = postgres({ connection: '...' })
143
+ await migrate() // run all pending migrations
144
+ await sql.close()
523
145
  ```
524
146
 
525
- ```tsx
526
- // Frontend — React hook
527
- import { useAgentStream } from 'weifuwu/react'
147
+ Options: `connection`, `max`, `ssl`, `idle_timeout`, `connect_timeout`, `statementTimeout`, `onQuery`, `signal`, `closeTimeout`
528
148
 
529
- const { getAgentText, isAgentStreaming, stream } = useAgentStream({
530
- wsPath: '/ws',
531
- channelId: 1,
532
- })
533
- ```
534
-
535
- Multi-round conversation context: the last 10 channel messages are automatically injected into agent calls.
536
-
537
- ---
149
+ Types:
538
150
 
539
- ## Test Utilities
151
+ - `PostgresOptions`, `PostgresClient`, `PostgresInjected`, `SqlClient`, `Sql`
540
152
 
541
- Chainable test helper for HTTP-level testing without starting a server:
153
+ #### redis()
542
154
 
543
155
  ```ts
544
- import { testApp } from 'weifuwu'
545
-
546
- const app = testApp()
547
- app.use(postgres({ connection: TEST_DB }))
548
- app.get('/users/:id', (req, ctx) => Response.json({ id: ctx.params.id, user: ctx.user }))
156
+ import { redis } from 'weifuwu'
549
157
 
550
- const res = await app
551
- .getReq('/users/42?name=Alice')
552
- .withUser({ id: 1 })
553
- .header('X-Custom', 'val')
554
- .body({ data: 'test' })
555
- .send()
158
+ app.use(redis({ url: process.env.REDIS_URL }))
159
+ // ctx.redis → Redis client
556
160
 
557
- assert.equal(res.status, 200)
558
- assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
161
+ await ctx.redis.set('key', 'value')
162
+ const val = await ctx.redis.get('key')
559
163
  ```
560
164
 
561
- | Method | Description |
562
- | ------------------------------------------------------------ | ----------------------------------------------------- |
563
- | `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request |
564
- | `.withUser(u)` `.withTenant(t)` `.with(ctx)` | Simulate middleware injection |
565
- | `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
566
- | `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
567
-
568
- **WebSocket testing** (new in v0.25) — `app.ws()` + `app.wsReq()`:
165
+ Options: `url`, `host`, `port`, `password`, `db`, `keyPrefix`, `maxRetriesPerRequest`, `enableReadyCheck`, `lazyConnect`, `retryStrategy`
569
166
 
570
- ```ts
571
- const app = testApp()
572
- app.ws('/echo', {
573
- open(ws) {
574
- ws.send(JSON.stringify({ type: 'connected' }))
575
- },
576
- message(ws, ctx, data) {
577
- ws.send('echo: ' + data.toString())
578
- },
579
- })
580
-
581
- // Connect via WebSocket
582
- const conn = await app.wsReq('/echo').connect()
583
-
584
- // Wait for the open message
585
- const openMsg = await conn.receiveJson()
586
- assert.equal(openMsg.type, 'connected')
167
+ Types: `RedisOptions`, `RedisClient`, `RedisInjected`, `Redis`
587
168
 
588
- // Send and receive
589
- conn.send('hello')
590
- const reply = await conn.receive()
591
- assert.equal(reply, 'echo: hello')
169
+ #### aiProvider()
592
170
 
593
- conn.close()
594
- await app.close() // cleanup server
595
- ```
596
-
597
- | Method | Description |
598
- | ------------------------------------------ | ------------------------------------------- |
599
- | `app.ws(path, handler)` | Register a WebSocket handler |
600
- | `app.wsReq(path)` | Start building a WebSocket connection |
601
- | `.timeout(ms)` | Set connection timeout (default: 5000) |
602
- | `.connect()` → `TestWSConnection` | Connect and return a connection handle |
603
- | `conn.send(data)` / `conn.json(obj)` | Send a message |
604
- | `conn.receive()` / `conn.receiveJson<T>()` | Wait for the next message |
605
- | `conn.expectSilent(ms)` | Assert no message arrives within the period |
606
- | `conn.close()` | Close the connection |
607
- | `app.close()` | Close all connections and stop the server |
171
+ ```ts
172
+ import { aiProvider } from 'weifuwu'
608
173
 
609
- ### Database test isolation
174
+ app.use(aiProvider())
175
+ // ctx.ai → AIProvider
610
176
 
611
- ```ts
612
- import { createTestDb, withTestDb } from 'weifuwu'
177
+ app.get('/ask', async (req, ctx) => {
178
+ const result = await ctx.ai.generateText({
179
+ prompt: 'Explain quantum computing',
180
+ })
181
+ return Response.json(result)
182
+ })
613
183
 
614
- // Isolated schema — each test gets its own schema, destroyed after
615
- const db = await createTestDb()
616
- await db.sql`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
617
- await db.sql`INSERT INTO users (name) VALUES ('Alice')`
618
- await db.destroy() // DROP SCHEMA ... CASCADE
619
-
620
- // Transaction rollback — all changes are rolled back after callback
621
- await withTestDb(async (sql) => {
622
- await sql`INSERT INTO users ...`
623
- // Automatically rolled back
184
+ // Streaming
185
+ app.get('/stream', async (req, ctx) => {
186
+ const result = ctx.ai.streamText({ prompt: 'Tell me a story' })
187
+ return result.toTextStreamResponse()
624
188
  })
625
189
  ```
626
190
 
627
- | Function | Description |
628
- | ---------------------- | --------------------------------------------------------------- |
629
- | `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
630
- | `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback |
631
-
632
- Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset.
633
-
634
- ---
635
-
636
- ## Module Reference
637
-
638
- Modules are organized alphabetically. Each module shows its pattern badge (`[α]` Middleware, `[β]` Router, `[γ]` Standalone, `[δ]` Client-side) and category.
191
+ Configured via environment variables:
639
192
 
640
- **Category key:** AI, API, Clientδ, Database, DevTools, Networking, Security, SSR, UX
193
+ - `OPENAI_API_KEY` API key (default: `ollama`)
194
+ - `OPENAI_BASE_URL` — API base URL (default: `http://localhost:11434/v1`)
195
+ - `OPENAI_MODEL` — model name (default: `gpt-4o`)
641
196
 
642
- ---
197
+ Types: `AIProviderOptions`, `AIProvider`, `AIProviderInjected`
643
198
 
644
- ### agent [β] [AI]
199
+ Also exports the raw SDK functions:
645
200
 
646
201
  ```ts
647
- const provider = aiProvider()
648
- const a = agent({ pg, provider })
649
- await a.migrate()
650
- app.use('/api', a)
651
- await a.addKnowledge(agentId, 'Title', 'some knowledge content')
652
- a.run(agentId, { input: 'summarize the data', stream: true })
202
+ import { streamText, generateText, embed, embedMany, tool, openai } from 'weifuwu'
653
203
  ```
654
204
 
655
- | Option | Type | Default | Description |
656
- | -------------------- | ------------ | ------------------------- | --------------------------------------------- |
657
- | `pg` | `object` | — | PostgreSQL client |
658
- | `provider` | `AIProvider` | `aiProvider()` (from env) | AI provider for model & embedding resolution |
659
- | `model` | `object` | — | Explicit AI model (overrides provider) |
660
- | `embeddingModel` | `object` | — | Explicit embedding model (overrides provider) |
661
- | `embeddingDimension` | `number` | `provider.dimension` | Embedding vector dimension |
662
- | `tools` | `object[]` | — | Custom tool definitions |
663
-
664
- | Method | Description |
665
- | ---------------------------------------------- | ------------------------ |
666
- | `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
667
- | `.addKnowledge(agentId, title, content)` | Add knowledge document |
668
- | `.migrate()` | DB setup |
669
- | `.close()` | Cleanup |
205
+ #### queue()
670
206
 
671
- ### aiStream [β] [AI]
207
+ ```ts
208
+ import { queue } from 'weifuwu'
672
209
 
673
- Creates an AI streaming chat endpoint using the Vercel AI SDK.
210
+ app.use(queue({ store: 'memory' }))
211
+ // ctx.queue → Queue
674
212
 
675
- ```ts
676
- const provider = aiProvider()
677
- const chat = await aiStream(async (req) => ({ messages: (await req.json()).messages }), provider)
678
- app.use('/chat', chat)
679
- ```
213
+ // In-memory queue (default)
214
+ const q = queue()
680
215
 
681
- | Param | Type | Description |
682
- | ---------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- |
683
- | `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
684
- | `provider` | `AIProvider` | Optional. If provided and handler omits `model`, `provider.model()` is used as default |
216
+ // Redis-backed queue
217
+ const q = queue({ store: 'redis', redis: ctx.redis })
685
218
 
686
- ### analytics [β] [API]
219
+ // PostgreSQL-backed queue
220
+ const q = queue({ store: 'pg', pg: { sql: ctx.sql } })
687
221
 
688
- In-memory or PostgreSQL page view tracking with built-in dashboard.
222
+ q.process('email', async (job) => {
223
+ await sendEmail(job.payload)
224
+ })
689
225
 
690
- ```ts
691
- const a = analytics()
692
- app.use(a.middleware())
693
- app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
226
+ await q.add('email', { to: 'user@example.com', subject: 'Hello' })
694
227
  ```
695
228
 
696
- | Option | Type | Default | Description |
697
- | ---------- | ---------- | --------------------------------------- | --------------------------------- |
698
- | `pg` | `object` | — | PostgreSQL client for persistence |
699
- | `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip |
229
+ Methods: `add(type, payload, opts?)`, `process(type, handler)`, `cron(pattern, handler)`, `run()`, `stats()`, `jobs(limit?)`, `failedJobs(limit?)`, `retryFailed(jobId)`, `retryAllFailed(type?)`, `close()`, `dashboard()`, `migrate()`
700
230
 
701
- ```ts
702
- // With PostgreSQL
703
- const a = analytics({ pg })
704
- await a.migrate()
705
- app.use(a.middleware())
706
- app.use('/', a) // dashboard routes
707
- ```
231
+ Types: `QueueOptions`, `Queue`, `QueueJob`, `QueueInjected`
708
232
 
709
- ### auth [α] [Security]
233
+ #### graphql()
710
234
 
711
235
  ```ts
712
- app.use(auth({ token: 'sk-123' })) // static token
713
- app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
714
- app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
715
- app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
236
+ import { graphql } from 'weifuwu'
716
237
 
717
- // Session-based auth (must be placed after session() middleware)
718
- app.use(session())
719
238
  app.use(
720
- auth({
721
- session: true,
722
- resolveUser: async (userId) => {
723
- // load user from DB
724
- const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
725
- return user ?? null // null destroy stale session
726
- },
239
+ '/graphql',
240
+ graphql({
241
+ schema: `
242
+ type Query { hello: String }
243
+ `,
244
+ resolvers: { Query: { hello: () => 'world' } },
727
245
  }),
728
246
  )
729
- ```
730
-
731
- | Option | Type | Default | Description |
732
- | ------------- | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------- |
733
- | `token` | `string` | — | Static token to match |
734
- | `header` | `string` | `'Authorization'` | Header name |
735
- | `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
736
- | `proxy` | `string` | — | Auth service URL to proxy requests to |
737
- | `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
738
- | `resolveUser` | `(userId) => object\|null` | — | Load user from userId (called when `session: true`). Return falsy to reject + auto-destroy stale session |
739
-
740
- When `session: true`, auth checks `ctx.session.userId` before the
741
- Authorization header. This lets logged-in users authenticate via their
742
- session cookie without sending a token. Falls back to header/token auth
743
- if no session userId is present.
744
247
 
745
- ### compress [α] [DevTools]
746
-
747
- ```ts
748
- app.use(compress()) // brotli > gzip > deflate (min 1KB)
749
- app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and level
248
+ // With GraphiQL IDE:
249
+ app.use(
250
+ '/graphql',
251
+ graphql({
252
+ schema: `type Query { hello: String }`,
253
+ graphiql: true,
254
+ }),
255
+ )
750
256
  ```
751
257
 
752
- | Option | Type | Default | Description |
753
- | ----------- | -------- | ------- | ----------------------------- |
754
- | `threshold` | `number` | `1024` | Minimum byte size to compress |
755
- | `level` | `number` | `6` | Compression level (zlib) |
258
+ Options: `schema`, `rootValue`, `resolvers`, `context`, `graphiql`, `maxDepth`, `timeout`
259
+
260
+ Types: `GraphQLOptions`, `GraphQLHandler`
756
261
 
757
- ### cors [α] [DevTools]
262
+ #### cors()
758
263
 
759
264
  ```ts
760
- app.use(cors()) // allow all
761
- app.use(cors({ origin: ['https://example.com'] })) // whitelist
762
- app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
763
- app.use(cors({ credentials: true, maxAge: 3600 }))
265
+ import { cors } from 'weifuwu'
266
+ app.use(cors({ origin: 'https://myapp.com' }))
764
267
  ```
765
268
 
766
- | Option | Type | Default | Description |
767
- | ---------------- | ---------------------------- | -------------------------------------------------------- | ---------------------------------- |
768
- | `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
769
- | `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
770
- | `allowedHeaders` | `string[]` | — | Custom allowed headers |
771
- | `exposedHeaders` | `string[]` | — | Response headers exposed to client |
772
- | `credentials` | `boolean` | `false` | Allow cookies/credentials |
773
- | `maxAge` | `number` | — | Preflight cache duration (seconds) |
269
+ Options: `origin`, `methods`, `allowedHeaders`, `exposedHeaders`, `credentials`, `maxAge`
774
270
 
775
- ### flash [α] [UX]
776
-
777
- Cookie-based flash message. Read from request, write via redirect.
271
+ #### compress()
778
272
 
779
273
  ```ts
780
- app.use(flash())
781
-
782
- app.get('/', (req, ctx) => {
783
- const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
784
- })
785
-
786
- app.post('/save', (req, ctx) => {
787
- return ctx.flash.set({ type: 'success', text: 'Saved!' }, '/articles')
788
- })
274
+ import { compress } from 'weifuwu'
275
+ app.use(compress({ threshold: 1024, brotli: true }))
789
276
  ```
790
277
 
791
- | Option | Type | Default | Description |
792
- | ------ | -------- | --------- | ----------- |
793
- | `name` | `string` | `'flash'` | Cookie name |
278
+ Options: `threshold`, `brotli`, `level`
794
279
 
795
- ### cache [α] [DevTools]
796
-
797
- Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
280
+ #### helmet()
798
281
 
799
282
  ```ts
800
- app.use(cache()) // in-memory, 5min TTL
801
- app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
802
- app.use(
803
- cache({
804
- ttl: 30_000,
805
- tag: (req, ctx) => (ctx.user ? `user:${ctx.user.id}` : undefined), // per-user invalidation
806
- }),
807
- )
808
-
809
- // Programmatic invalidation
810
- const c = cache({ store: 'redis', redis: ctx.redis })
811
- app.use(c)
812
- await c.invalidate('users') // invalidate all entries tagged with 'users'
813
- await c.flush() // clear entire cache
283
+ import { helmet } from 'weifuwu'
284
+ app.use(helmet())
814
285
  ```
815
286
 
816
- | Option | Type | Default | Description |
817
- | -------------- | ----------------------------------- | ------------------ | --------------------------------------------- |
818
- | `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
819
- | `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
820
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
821
- | `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
822
- | `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
823
- | `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
824
- | `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
825
- | `maxBodySize` | `number` | `1048576` (1MB) | Max body bytes to cache |
287
+ Sets security headers (CSP, HSTS, X-Frame-Options, etc.).
826
288
 
827
- Cached responses include `X-Cache: HIT` and `Age` headers. Requests with `Authorization` or `Cookie` headers are never cached. Binary content types (image, audio, video) are skipped.
289
+ #### rateLimit()
828
290
 
829
291
  ```ts
830
- import { MemoryCache, RedisCache } from 'weifuwu'
831
-
832
- const mem = new MemoryCache()
833
- await mem.set(
834
- 'key',
835
- { status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] },
836
- 300_000,
837
- )
838
- mem.close()
839
- ```
292
+ import { rateLimit } from 'weifuwu'
840
293
 
841
- ### csrf [α] [Security]
294
+ // In-memory (default)
295
+ app.use(rateLimit({ window: 60, max: 100 }))
842
296
 
843
- ```ts
844
- app.use(csrf())
845
- // ctx.csrf.token — set on GET/HEAD/OPTIONS
846
- // Auto-validates x-csrf-token or x-xsrf-token header on POST/PUT/DELETE/PATCH
847
- // Falls back to body field matching the key name
297
+ // Redis-backed
298
+ app.use(rateLimit({ window: 60, max: 100, redis: ctx.redis }))
848
299
  ```
849
300
 
850
- | Option | Default | Description |
851
- | ---------------- | -------------------------- | ----------------------------------------- |
852
- | `cookie` | `'_csrf'` | Cookie name |
853
- | `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
854
- | `key` | `'_csrf'` | Body field fallback |
855
- | `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
856
-
857
- ### deploy [β] [Networking]
301
+ Options: `window`, `max`, `redis`, `key`, `statusCode`, `message`
858
302
 
859
- Multi-process manager with reverse proxy, health checks, auto-restart, and zero-downtime updates. Works identically locally and in production.
303
+ #### validate()
860
304
 
861
305
  ```ts
862
- import { deploy, defineConfig } from 'weifuwu'
306
+ import { validate } from 'weifuwu'
307
+ import { z } from 'zod'
863
308
 
864
- // Local
865
- await deploy(
866
- defineConfig({
867
- apps: { blog: {}, api: {} },
309
+ app.post(
310
+ '/users',
311
+ validate({
312
+ body: z.object({ name: z.string() }),
313
+ query: z.object({ ref: z.string().optional() }),
314
+ params: z.object({}),
315
+ headers: z.object({ authorization: z.string() }),
868
316
  }),
317
+ handler,
869
318
  )
319
+ // ctx.parsed → { body, query, params, headers }
870
320
 
871
- // Production
872
- await deploy(
873
- defineConfig({
874
- domain: 'example.com',
875
- deployToken: process.env.DEPLOY_TOKEN,
876
- apps: { blog: {}, api: {} },
877
- }),
878
- )
321
+ function handler(req, ctx) {
322
+ const { name } = ctx.parsed.body
323
+ }
879
324
  ```
880
325
 
881
- **Auto-derived defaults** — each app key derives `dir`, `port`, `entry`, and `path`:
882
-
883
- | Field | Default | Rule |
884
- | ------- | ------------ | -------------------------- |
885
- | `dir` | App key | `blog` → `'./blog'` |
886
- | `entry` | `'index.ts'` | Default entry file |
887
- | `port` | `3001+` | Auto-incremented from 3001 |
888
- | `path` | `'/key'` | Only for localhost domain |
889
-
890
- Override any field explicitly:
326
+ #### upload()
891
327
 
892
328
  ```ts
893
- defineConfig({
894
- apps: {
895
- blog: { dir: '../packages/blog', entry: 'server.ts', port: 8080, path: '/blog' },
896
- },
897
- })
898
- ```
899
-
900
- **Routing** — match priority: explicit path > app key > defaultApp.
329
+ import { upload } from 'weifuwu'
901
330
 
902
- ```ts
903
- apps: {
904
- api: { path: '/api' }, // example.com/api or localhost:3000/api
905
- blog: {}, // blog.example.com or localhost:3000/blog
906
- }
331
+ app.post('/files', upload({ maxFiles: 5, maxSize: 10 * 1024 * 1024 }), handler)
332
+ // ctx.parsed → { files: UploadedFile[], fields: Record<string, string> }
907
333
  ```
908
334
 
909
- **Blue-green** zero-downtime via `ports`:
335
+ Options: `maxFiles`, `maxSize`, `allowedTypes`, `keepExtensions`
336
+
337
+ #### static()
910
338
 
911
339
  ```ts
912
- apps: {
913
- blog: {
914
- ports: [3001, 3002]
915
- }
916
- }
340
+ import { serveStatic } from 'weifuwu'
341
+ app.use('/assets', serveStatic({ root: './public', index: 'index.html' }))
917
342
  ```
918
343
 
919
- **WebSocket** automatically bridged through the gateway.
920
-
921
- **Process watchdog** — auto-restarts with exponential backoff on unexpected exit.
922
-
923
- **Management API** — all endpoints require `Authorization: Bearer <deployToken>`:
924
-
925
- | Endpoint | Method | Description |
926
- | ----------------------------- | ------ | -------------- |
927
- | `/_deploy/apps` | GET | List apps |
928
- | `/_deploy/apps/:name` | GET | App details |
929
- | `/_deploy/apps/:name/deploy` | POST | Restart |
930
- | `/_deploy/apps/:name/restart` | POST | Restart |
931
- | `/_deploy/apps/:name/stop` | POST | Stop |
932
- | `/_deploy/apps/:name/start` | POST | Start |
933
- | `/_deploy/apps/:name/logs` | GET | SSE log stream |
934
-
935
- ```bash
936
- curl -H "Authorization: Bearer my-token" http://localhost:3000/_deploy/apps
937
- ```
344
+ Options: `root`, `index`, `maxAge`, `immutable`, `brotli`, `headers`
938
345
 
939
- **Running** — use systemd for production:
346
+ #### csrf()
940
347
 
941
- ```ini
942
- [Service]
943
- WorkingDirectory=/opt/deploy
944
- ExecStart=/usr/bin/node /opt/deploy/deploy.ts
945
- Restart=always
348
+ ```ts
349
+ import { csrf } from 'weifuwu'
350
+ app.use(csrf())
351
+ // ctx.csrf.token → string (for forms)
946
352
  ```
947
353
 
948
- **DeployConfig:**
949
-
950
- | Option | Default | Description |
951
- | ------------- | ------------- | ------------------------------- |
952
- | `domain` | `'localhost'` | Root domain |
953
- | `port` | `3000` | Gateway port |
954
- | `deployToken` | — | Bearer token for management API |
955
- | `defaultApp` | — | Fallback route |
956
- | `apps` | — | `Record<string, AppConfig>` |
957
-
958
- **AppConfig:**
959
-
960
- | Field | Default | Description |
961
- | ---------------- | ---------------- | ------------------------------- |
962
- | `dir` | App key | Directory containing the app |
963
- | `port` | Auto (3001+) | Internal port |
964
- | `entry` | `'index.ts'` | Entry file |
965
- | `path` | `'/key'` (local) | URL path prefix |
966
- | `env` | — | Environment variables |
967
- | `healthEndpoint` | `/` | Health check path |
968
- | `buildCommand` | — | Build command |
969
- | `ports` | — | `[port, port+1]` for blue-green |
970
-
971
- ### env [α] [DevTools]
972
-
973
- Environment variable middleware. Injects `ctx.env` with all `WEIFUWU_PUBLIC_*` variables (prefix stripped).
974
- Safe to expose to the client.
975
-
976
- ```ts
977
- import { env, loadEnv } from 'weifuwu'
978
- loadEnv() // Load .env into process.env
979
- app.use(env()) // → ctx.env
354
+ Protects POST/PUT/DELETE endpoints by requiring a valid CSRF token in `X-CSRF-Token` header.
980
355
 
981
- app.get('/config', (req, ctx) => {
982
- return Response.json({ apiUrl: ctx.env.API_URL })
983
- })
984
- ```
356
+ Options: `secret`, `cookie`, `header`
985
357
 
986
- Helper utilities:
358
+ #### flash()
987
359
 
988
360
  ```ts
989
- import { isDev, isProd, isBundled, getPublicEnv } from 'weifuwu'
990
-
991
- isDev() // NODE_ENV === 'development'
992
- isProd() // NODE_ENV === 'production'
993
- isBundled() // Running from compiled dist/index.js?
994
- getPublicEnv() // { API_URL: '...' } — no middleware needed
361
+ import { flash } from 'weifuwu'
362
+ app.use(flash())
363
+ // ctx.flash.value string | undefined (read-once)
364
+ // ctx.flash.set('success', 'Saved!')
995
365
  ```
996
366
 
997
- | Function | Description |
998
- | ---------------- | ---------------------------------------------------------------- |
999
- | `loadEnv(path?)` | Load `.env` file into `process.env` (does not override existing) |
1000
- | `env()` | Middleware — injects `ctx.env` with public vars |
1001
- | `getPublicEnv()` | Returns `WEIFUWU_PUBLIC_*` vars with prefix stripped |
1002
- | `isDev()` | `true` when `NODE_ENV === 'development'` |
1003
- | `isProd()` | `true` when `NODE_ENV === 'production'` |
1004
- | `isBundled()` | `true` when running from compiled bundle |
367
+ Options: `cookie`, `maxAge`
1005
368
 
1006
- ### graphql [β] [API]
369
+ #### requestId()
1007
370
 
1008
371
  ```ts
1009
- const handler: GraphQLHandler = () => ({
1010
- schema: `type Query { hello: String }`,
1011
- resolvers: { Query: { hello: () => 'world' } },
1012
- graphiql: true, // GET / returns GraphiQL IDE
1013
- maxDepth: 10, // max query nesting (default 10, 0 = disable)
1014
- timeout: 30_000, // execution timeout in ms
1015
- })
1016
- app.use('/graphql', graphql(handler))
372
+ import { requestId } from 'weifuwu'
373
+ app.use(requestId())
374
+ // ctx.requestId string (UUID)
375
+ // Response gets X-Request-Id header
1017
376
  ```
1018
377
 
1019
- | Option | Type | Default | Description |
1020
- | ----------- | ------------------------- | -------- | ------------------------------ |
1021
- | `schema` | `string \| GraphQLSchema` | — | SDL string or pre-built schema |
1022
- | `resolvers` | `object` | — | Resolver map |
1023
- | `rootValue` | `any` | — | Root value for queries |
1024
- | `context` | `(req, ctx) => object` | — | Per-request context factory |
1025
- | `graphiql` | `boolean` | `false` | Serve GraphiQL IDE at GET / |
1026
- | `maxDepth` | `number` | `10` | Max query nesting depth |
1027
- | `timeout` | `number` | `30_000` | Execution timeout (ms) |
378
+ Options: `header`, `generator`
1028
379
 
1029
- ### health [β] [API]
380
+ #### health()
1030
381
 
1031
382
  ```ts
383
+ import { health } from 'weifuwu'
1032
384
  app.use('/health', health())
1033
- // Returns 200 on success, 503 when check throws
385
+ // GET /health { status: 'ok', uptime: 12345 }
1034
386
  ```
1035
387
 
1036
- | Option | Type | Default | Description |
1037
- | ------- | --------------------- | ----------- | ---------------------------- |
1038
- | `path` | `string` | `'/health'` | Health check endpoint |
1039
- | `check` | `() => Promise<void>` | — | Async function; throws → 503 |
1040
-
1041
- ### helmet [α] [Security]
1042
-
1043
- 15 security headers: CSP, HSTS, X-Frame-Options, X-Content-Type-Options, etc.
388
+ #### theme()
1044
389
 
1045
390
  ```ts
1046
- app.use(helmet())
1047
- app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))
391
+ import { theme } from 'weifuwu'
392
+ app.use(theme({ cookie: 'theme' }))
393
+ // ctx.theme → { value: 'light' | 'dark', set(newValue) }
1048
394
  ```
1049
395
 
1050
- | Option | Default | Description |
1051
- | --------------------------- | --------------------------------------- | -------------------------- |
1052
- | `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
1053
- | `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
1054
- | `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
1055
- | `referrerPolicy` | `'no-referrer'` | Referrer header |
1056
- | `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
1057
- | `permissionsPolicy` | — | Feature permissions policy |
1058
- | `crossOriginEmbedderPolicy` | — | COEP header |
1059
- | `crossOriginOpenerPolicy` | — | COOP header |
1060
- | `crossOriginResourcePolicy` | — | CORP header |
1061
-
1062
- ### iii [β] — Worker / Function / Trigger [API]
396
+ Options: `cookie`, `default`, `param`
1063
397
 
1064
- Distributed function execution with WebSocket workers, triggers, and Redis streams.
398
+ #### i18n()
1065
399
 
1066
400
  ```ts
1067
- import { createWorker } from 'weifuwu'
1068
- const engine = iii({ pg, redis })
1069
- app.use('/iii', engine)
1070
- app.ws('/iii', engine.wsHandler())
1071
-
1072
- const w = createWorker('orders')
1073
- w.registerFunction('orders::create', async (payload) =>
1074
- db.query('INSERT INTO orders ...', [payload.items]),
1075
- )
1076
- engine.addWorker(w)
1077
- await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
401
+ import { i18n } from 'weifuwu'
402
+ app.use(i18n({ dir: './locales', defaultLocale: 'en' }))
403
+ // ctx.i18n → { locale: 'en', t(key), set(locale) }
1078
404
  ```
1079
405
 
1080
- | Option | Type | Default | Description |
1081
- | ----------- | -------- | ------- | --------------------------------------------- |
1082
- | `pg` | `object` | — | PostgreSQL client for persistent triggers |
1083
- | `redis` | `object` | — | Redis client for streams |
1084
- | `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
1085
-
1086
- | Method | Description |
1087
- | ---------------------------------------------------------- | ------------------------- |
1088
- | `.addWorker(w)` | Register a worker |
1089
- | `.removeWorker(w)` | Remove a worker |
1090
- | `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
1091
- | `.listWorkers()` | List registered workers |
1092
- | `.listFunctions()` | List registered functions |
1093
- | `.listTriggers()` | List registered triggers |
1094
- | `.wsHandler()` | WebSocket handler |
1095
- | `.migrate()` | DB setup |
1096
- | `.shutdown()` | Clean shutdown |
1097
-
1098
- ### knowledgeBase [β] — RAG with pgvector [AI]
406
+ Options: `dir`, `defaultLocale`, `cookie`, `param`, `header`
1099
407
 
1100
- ```ts
1101
- import { knowledgeBase, aiProvider } from 'weifuwu'
408
+ ### Standalone utilities
1102
409
 
1103
- const kb = knowledgeBase({
1104
- pg: postgres(),
1105
- provider: aiProvider(),
1106
- table: 'my_docs',
1107
- })
410
+ #### mailer()
1108
411
 
1109
- // Create table + HNSW index (safe to call multiple times)
1110
- await kb.migrate()
412
+ ```ts
413
+ import { mailer } from 'weifuwu'
1111
414
 
1112
- // Ingest a document (auto chunk → embed → store)
1113
- await kb.ingest('docs/intro.md', `# Welcome\n\nThis is the introduction...`, {
1114
- title: 'Introduction',
1115
- metadata: { source: 'docs', author: 'alice' },
415
+ const m = mailer({
416
+ host: 'smtp.example.com',
417
+ port: 587,
418
+ auth: { user: '...', pass: '...' },
419
+ from: 'noreply@example.com',
1116
420
  })
1117
421
 
1118
- // Semantic search
1119
- const results = await kb.search('how to get started?', { limit: 5 })
1120
- // → [{ key, title, content, score: 0.92, metadata }, ...]
1121
-
1122
- // Delete
1123
- await kb.delete('docs/outdated.md')
1124
-
1125
- // List all documents
1126
- const entries = await kb.list()
1127
- // → [{ key, title, chunks: 3 }, ...]
1128
-
1129
- // Use as middleware (injects ctx.kb.search)
1130
- app.use(kb.middleware())
1131
- app.get('/search', async (req, ctx) => {
1132
- const results = await ctx.kb.search(ctx.query.q)
1133
- return Response.json(results)
1134
- })
422
+ await m.send({ to: 'user@example.com', subject: 'Hello', text: '...' })
423
+ await m.close()
1135
424
  ```
1136
425
 
1137
- | Option | Type | Default | Description |
1138
- | ----------------- | ---------------- | ------------ | --------------------------------------- |
1139
- | `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
1140
- | `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
1141
- | `table` | `string` | `'_kb_docs'` | Database table name |
1142
- | `chunkSize` | `number` | `512` | Max characters per chunk |
1143
- | `chunkOverlap` | `number` | `64` | Overlap between chunks |
1144
- | `searchLimit` | `number` | `5` | Default search result count |
1145
- | `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
1146
-
1147
- Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
1148
- replaces old chunks. Provider's `embed()` is used automatically.
1149
- The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
426
+ Options: `host`, `port`, `auth`, `from`, `secure`
1150
427
 
1151
- ### logdb [β] [API]
1152
-
1153
- PostgreSQL structured event logging with monthly partitioning.
428
+ #### SSE
1154
429
 
1155
430
  ```ts
1156
- const logger = logdb({ pg })
1157
- await logger.migrate()
1158
- app.use('/logs', logger)
1159
- await logger.clean(12) // drop partitions older than 12 months
1160
- await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
1161
- ```
1162
-
1163
- | Option | Type | Default | Description |
1164
- | ------- | -------- | ---------------- | ----------------- |
1165
- | `pg` | `object` | — | PostgreSQL client |
1166
- | `table` | `string` | `'_log_entries'` | Table name |
431
+ import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
1167
432
 
1168
- | Method | Path | Description |
1169
- | ------ | ------ | ---------------------------------------------------------------- |
1170
- | POST | `/` | Create log entry |
1171
- | GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
1172
- | GET | `/:id` | Get single entry |
433
+ const stream = createSSEStream()
434
+ stream.write(formatSSE('eventType', { data: 'hello' }))
435
+ stream.end()
436
+ ```
1173
437
 
1174
- ### logger [α] [DevTools]
438
+ #### Hub (pub/sub)
1175
439
 
1176
440
  ```ts
1177
- app.use(logger()) // GET /hello 200 5ms
1178
- app.use(logger({ format: 'combined' })) // with query params
1179
- ```
441
+ import { createHub } from 'weifuwu'
1180
442
 
1181
- | Option | Type | Default | Description |
1182
- | -------- | --------------------------------- | --------- | ------------------------------------------------------------- |
1183
- | `format` | `'short' \| 'combined' \| 'json'` | `'short'` | Log format: path only, path + query params, or JSON to stderr |
443
+ const hub = createHub({ redis: optionalRedisClient })
444
+ hub.join('room:1', ws)
445
+ hub.sendRoom('room:1', { type: 'message', text: 'hello' })
446
+ hub.leave(ws)
447
+ ```
1184
448
 
1185
- ### mailer [γ] [Networking]
449
+ #### Cookie helpers
1186
450
 
1187
451
  ```ts
1188
- const mail = mailer({
1189
- from: 'noreply@example.com',
1190
- transport: 'smtp://user:pass@smtp.example.com:587',
1191
- })
1192
- await mail.send({
1193
- to: 'user@test.com',
1194
- subject: 'Hello',
1195
- text: 'Body',
1196
- html: '<p>Body</p>',
1197
- cc: 'admin@test.com',
1198
- })
452
+ import { getCookies, setCookie, deleteCookie } from 'weifuwu'
1199
453
  ```
1200
454
 
1201
- | Option | Type | Default | Description |
1202
- | ----------- | ---------------- | ------- | ------------------------------------------------ |
1203
- | `transport` | `string\|object` | — | Nodemailer transport config or connection string |
1204
- | `from` | `string` | — | Default sender address |
1205
- | `send` | `function` | — | Custom send function (alternative to transport) |
1206
-
1207
- ### mcpClient [γ] — MCP Server integration [AI]
1208
-
1209
- [Model Context Protocol](https://modelcontextprotocol.io) client. Spawns MCP server
1210
- subprocesses and exposes their tools as AI SDK-compatible tool objects.
455
+ ### Test utilities
1211
456
 
1212
457
  ```ts
1213
- import { mcpClient, agent, aiProvider } from 'weifuwu'
1214
-
1215
- const fsMcp = mcpClient({
1216
- command: 'npx',
1217
- args: ['@modelcontextprotocol/server-filesystem', '/workspace'],
1218
- })
1219
-
1220
- const tools = await fsMcp.getTools()
458
+ import { testApp, TestApp, createTestDb, withTestDb } from 'weifuwu'
1221
459
 
1222
- const a = agent({ pg, provider: aiProvider(), tools })
1223
- await a.run(agentId, { input: 'read package.json' })
460
+ const app = new Router().handler()
461
+ const res = await testApp(app, new Request('http://localhost/'))
462
+ // res.status, res.headers, await res.json()
1224
463
 
1225
- // Later, refresh tools if the server provides new ones
1226
- await fsMcp.refresh()
1227
-
1228
- // Or call a tool directly
1229
- const result = await fsMcp.callTool('echo', { text: 'hello' })
1230
-
1231
- await fsMcp.close() // shutdown the MCP server process
464
+ // With database
465
+ const db = await createTestDb()
466
+ // db.sql, db.close()
1232
467
  ```
1233
468
 
1234
- | Option | Type | Default | Description |
1235
- | ----------------- | ---------- | ------- | ------------------------------------------------------- |
1236
- | `command` | `string` | — | **Required.** Command to spawn (e.g. `'npx'`, `'node'`) |
1237
- | `args` | `string[]` | `[]` | Arguments passed to the command |
1238
- | `env` | `object` | — | Extra environment variables |
1239
- | `timeout` | `number` | `15000` | Handshake/response timeout (ms) |
1240
- | `maxResponseSize` | `number` | `10MB` | Max tool response body size |
1241
-
1242
- | Method | Description |
1243
- | ------------ | ------------------------------------------------------------------------- |
1244
- | `getTools()` | Fetch tool definitions, returns `Record<string, Tool>`-compatible objects |
1245
- | `refresh()` | Re-fetch tool definitions from the server |
1246
- | `callTool()` | Call a tool by name directly |
1247
- | `close()` | Shutdown the MCP server process |
1248
-
1249
- Tool schemas (JSON Schema) are automatically converted to Zod schemas for AI SDK compatibility.
1250
- Responses are concatenated from text content items, with size limiting.
1251
-
1252
- ### oauthLogin (via user()) — Social login (OAuth 2.0 client) [Security]
1253
-
1254
- Social login is built into the [`user()`](#user-β) module via the `oauthLogin` option — no separate import needed.
469
+ ### Environment
1255
470
 
1256
471
  ```ts
1257
- app.use(session()) // required stores OAuth state
1258
- const u = user({
1259
- pg,
1260
- jwtSecret: process.env.JWT_SECRET!,
1261
- oauthLogin: {
1262
- redirectUrl: '/dashboard',
1263
- providers: {
1264
- google: {
1265
- clientId: process.env.GOOGLE_CLIENT_ID,
1266
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
1267
- },
1268
- github: {
1269
- clientId: process.env.GITHUB_CLIENT_ID,
1270
- clientSecret: process.env.GITHUB_CLIENT_SECRET,
1271
- },
1272
- },
1273
- },
1274
- })
1275
- await u.migrate()
1276
- app.use(u) // POST /register, POST /login, GET /auth/:provider, GET /auth/:provider/callback
1277
- ```
472
+ import { loadEnv, isDev, isProd, env } from 'weifuwu'
1278
473
 
1279
- **Flow:** User clicks "Login with Google" → redirected to Google → back to app → user created/linked in database → JWT signed → session created → redirected to `redirectUrl` with `?token=` (or JSON response for API clients).
474
+ loadEnv() // loads .env file
475
+ isDev() // NODE_ENV === 'development'
476
+ isProd() // NODE_ENV === 'production'
477
+ env('MY_VAR', 'default')
478
+ getPublicEnv() // env vars starting with PUBLIC_
479
+ ```
1280
480
 
1281
- Supports custom providers via `authUrl`, `tokenUrl`, `userUrl`, and `parseUser`:
481
+ ### Tracing
1282
482
 
1283
483
  ```ts
1284
- const u = user({
1285
- pg,
1286
- jwtSecret: process.env.JWT_SECRET!,
1287
- oauthLogin: {
1288
- providers: {
1289
- discord: {
1290
- clientId: process.env.DISCORD_CLIENT_ID,
1291
- clientSecret: process.env.DISCORD_CLIENT_SECRET,
1292
- authUrl: 'https://discord.com/api/oauth2/authorize',
1293
- tokenUrl: 'https://discord.com/api/oauth2/token',
1294
- userUrl: 'https://discord.com/api/users/@me',
1295
- parseUser: (data) => ({
1296
- id: data.id,
1297
- email: data.email ?? '',
1298
- name: data.global_name ?? data.username,
1299
- avatarUrl: data.avatar
1300
- ? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
1301
- : '',
1302
- }),
1303
- },
1304
- },
1305
- },
484
+ import { trace, currentTraceId } from 'weifuwu'
485
+
486
+ trace('fetch-user', async () => {
487
+ // auto-tracked with trace ID
1306
488
  })
489
+ currentTraceId() // get current trace ID
1307
490
  ```
1308
491
 
1309
- | Option (oauthLogin) | Type | Default | Description |
1310
- | ------------------- | ------------------------------------- | ------- | ------------------------------------------------------------------- |
1311
- | `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
1312
- | `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
1313
-
1314
- Built-in providers (Google, GitHub) have preset URLs — you only need to provide `clientId` and `clientSecret`. The module auto-creates a `_auth_providers` table on first request.
1315
-
1316
- ### messager [β] [Networking]
1317
-
1318
- Real-time chat with channels, WebSocket, agent routing.
492
+ ### Error handling
1319
493
 
1320
494
  ```ts
1321
- const msg = messager({ pg, agents, redis: redis() })
1322
- await msg.migrate()
1323
- app.use('/api', msg)
1324
- app.ws('/ws', msg.wsHandler())
1325
- await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
1326
- ```
495
+ import { HttpError } from 'weifuwu'
1327
496
 
1328
- | Option | Type | Default | Description |
1329
- | ---------------- | ------------- | ------- | ------------------------ |
1330
- | `pg` | `object` | — | PostgreSQL client |
1331
- | `agents` | `AgentModule` | — | Agent module for routing |
1332
- | `webhookTimeout` | `number` | — | Webhook timeout |
1333
- | `redis` | `object` | — | Redis client |
497
+ throw new HttpError('Not found', 404) // caught by serve(), returns 404
498
+ ```
1334
499
 
1335
- | Method | Description |
1336
- | -------------------------------- | --------------------------------------------------- |
1337
- | `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
1338
- | `.send(channel, content, opts?)` | Send message to channel |
1339
- | `.close()` | Cleanup |
500
+ ---
1340
501
 
1341
- ### notifier [α] [UX]
502
+ ## CLI
1342
503
 
1343
- Multi-channel notification system with inbox (DB persistent), email, and
1344
- WebSocket push. Per-user channel preferences.
504
+ ```bash
505
+ npx weifuwu init my-api
506
+ cd my-api
507
+ npm run dev
508
+ ```
1345
509
 
1346
- ```ts
1347
- import { notifier, mailer } from 'weifuwu'
510
+ Creates a minimal API project with `app.ts`, `index.ts`, and TypeScript config.
1348
511
 
1349
- const mail = mailer({ from: 'noreply@example.com', transport: '...' })
1350
- const n = notifier({ sql: pg.sql, mailer: mail })
1351
- await n.migrate()
1352
- app.use(n) // injects ctx.notifier
512
+ ---
1353
513
 
1354
- // Send a notification (routes through user's channel preferences)
1355
- await ctx.notifier.send(
1356
- { userId: 42, email: 'user@example.com' },
1357
- { title: 'Welcome!', body: 'Thanks for joining', type: 'onboarding' },
1358
- )
1359
-
1360
- // Broadcast to all users with inbox enabled
1361
- await ctx.notifier.broadcast({
1362
- title: 'System maintenance tonight',
1363
- body: 'The system will be down from 2-4 AM',
1364
- })
1365
-
1366
- // Check unread count
1367
- const count = await ctx.notifier.unreadCount(userId)
1368
-
1369
- // List notifications (newest first)
1370
- const notifications = await ctx.notifier.list(userId, { limit: 10 })
1371
-
1372
- // Mark as read
1373
- await ctx.notifier.markRead(userId, [notifId])
1374
- await ctx.notifier.markRead(userId) // mark ALL as read
1375
-
1376
- // User preferences
1377
- await ctx.notifier.setPreferences(userId, { channels: ['inbox', 'email'] })
1378
- const prefs = await ctx.notifier.getPreferences(userId)
1379
- // → { channels: ['inbox', 'email'] }
1380
- ```
1381
-
1382
- | Option | Type | Default | Description |
1383
- | ---------- | ----------- | ------------------ | ------------------------------- |
1384
- | `sql` | `SqlClient` | — | **Required.** PostgreSQL client |
1385
- | `mailer` | `Mailer` | — | Mailer for email channel |
1386
- | `hub` | `Hub` | — | Pub/sub hub for WebSocket push |
1387
- | `table` | `string` | `'_notifications'` | Notifications table name |
1388
- | `pageSize` | `number` | `50` | Default page size for list() |
1389
-
1390
- | Method | Description |
1391
- | -------------------------------- | --------------------------------------------- |
1392
- | `.send(to, message)` | Send notification (routes by user preference) |
1393
- | `.broadcast(message)` | Send to all users with inbox enabled |
1394
- | `.unreadCount(userId)` | Count unread notifications |
1395
- | `.count(userId, unreadOnly?)` | Total or unread count |
1396
- | `.markRead(userId, ids?)` | Mark notification(s) as read |
1397
- | `.list(userId, opts?)` | List notifications (paginated) |
1398
- | `.getPreferences(userId)` | Get user's channel preferences |
1399
- | `.setPreferences(userId, prefs)` | Set user's channel preferences |
1400
- | `.migrate()` | Create tables |
1401
- | `.clean(days)` | Delete notifications older than N days |
1402
-
1403
- **Channel routing:** Each user has channel preferences (default: `['inbox']`). When
1404
- `sending`, the notification is delivered to each enabled channel. Email requires
1405
- `mailer` to be configured. WebSocket requires `hub` (e.g. from `messager.wsHandler()`).
1406
-
1407
- ### opencode [β] [AI]
1408
-
1409
- AI programming assistant.
1410
-
1411
- ```ts
1412
- const oc = await opencode({
1413
- pg,
1414
- model: openai('gpt-4o'),
1415
- workspace: '/home/user/project',
1416
- permissions: { bash: { allow: true }, write: { allow: false } },
1417
- })
1418
- await oc.migrate()
1419
- app.use('/opencode', oc)
1420
- app.ws('/opencode', oc.wsHandler())
1421
- ```
1422
-
1423
- | Option | Type | Default | Description |
1424
- | -------------- | ---------- | ------- | ------------------------------------------------------ |
1425
- | `pg` | `object` | — | PostgreSQL client |
1426
- | `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
1427
- | `baseURL` | `string` | — | OpenAI-compatible API base URL |
1428
- | `apiKey` | `string` | — | API key for the model |
1429
- | `workspace` | `string` | — | Project directory |
1430
- | `systemPrompt` | `string` | — | Custom system prompt |
1431
- | `skills` | `object[]` | — | Custom skill definitions |
1432
- | `permissions` | `object` | — | Tool permission rules |
1433
-
1434
- ### postgres [α] [Database]
1435
-
1436
- Type-safe PostgreSQL client with schema builder, CRUD, migrations, soft delete, and JSONB/vector support.
1437
-
1438
- ```ts
1439
- const pg = postgres() // reads DATABASE_URL
1440
- app.use(pg) // injects ctx.sql
1441
- ```
1442
-
1443
- | Option | Type | Default | Description |
1444
- | ------------------ | --------------------------- | ------------------ | --------------------------------------- |
1445
- | `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
1446
- | `max` | `number` | `10` | Max pool connections |
1447
- | `ssl` | `boolean\|object` | — | SSL options |
1448
- | `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
1449
- | `connect_timeout` | `number` | `30` | Connection timeout |
1450
- | `statementTimeout` | `number` | `30_000` | Per-statement timeout (ms, 0 = disable) |
1451
- | `onQuery` | `(query, ms, rows) => void` | — | Query logging callback |
1452
-
1453
- ```ts
1454
- // Raw SQL via tagged template
1455
- await pg.sql`SELECT * FROM users WHERE email = ${email}`
1456
-
1457
- // Define a table — one API, sql pre-bound
1458
- import { serial, text, boolean, timestamps } from 'weifuwu'
1459
-
1460
- const users = pg.table('_users', {
1461
- id: serial('id').primaryKey(),
1462
- name: text('name').notNull(),
1463
- email: text('email').unique().notNull(),
1464
- active: boolean('active').default(true),
1465
- ...timestamps(),
1466
- })
1467
- await users.create() // DDL — no need to pass sql
1468
- await users.createIndex('email')
1469
-
1470
- // CRUD — sql already bound
1471
- await users.insert({ name: 'Alice' })
1472
- const { count, data } = await users.readMany(
1473
- { role: 'admin' },
1474
- { orderBy: { name: 'asc' }, limit: 10 },
1475
- )
1476
- await users.upsert({ email: 'alice@test.com' }, 'email')
1477
-
1478
- // Reuse schema without redefining fields
1479
- import { pgTable } from 'weifuwu'
1480
- const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
1481
- const users = pg.table(usersSchema) // bind — no field duplication
1482
-
1483
- // Transactions — with auto-retry on deadlock/serialization failure
1484
- await pg.transaction(
1485
- async (sql) => {
1486
- const txUsers = users.withSql(sql)
1487
- return txUsers.insert({ name: 'Bob' })
1488
- },
1489
- { maxRetries: 3 },
1490
- )
1491
-
1492
- // Soft delete — automatic if deleted_at column exists
1493
- await users.delete(1) // SET deleted_at = NOW()
1494
- await users.hardDelete(1) // DELETE FROM
1495
- await users.read(1) // auto-filters deleted_at IS NULL (use withDeleted: true to include)
1496
-
1497
- // JSONB queries
1498
- const logs = pg.table('logs', { meta: jsonb<{ service: string }>('meta') })
1499
- await logs.readMany(contains('meta', { service: 'auth' }))
1500
-
1501
- // Connection pool visibility
1502
- console.log(pg.poolStats()) // { active: 3, idle: 7, waiting: 0, max: 10 }
1503
-
1504
- // Migration tracking
1505
- await pg.migrate() // creates _weifuwu_migrations
1506
- await pg.markMigrated('myModule') // idempotent
1507
- const done = await pg.isMigrated('myModule')
1508
-
1509
- // Partitioned tables
1510
- await logs.create({ partitionBy: partitionBy('range', 'created_at') })
1511
- ```
1512
-
1513
- **When to use pgTable vs pg.table:**
1514
- | API | Use when |
1515
- |-----|---------|
1516
- | `pg.table('t', cols)` | You have `pg` available (factory, handler, migrate) |
1517
- | `pg.table(schema)` | Reusing a schema without duplicating field definitions |
1518
- | `pgTable('t', cols)` | No `pg` reference (utility modules, standalone schema files) |
1519
-
1520
- | Column builder | Type | Notes |
1521
- | ---------------------------------- | ---------- | ------------------------------- |
1522
- | `serial(name)` | `number` | Auto-increment |
1523
- | `uuid(name)` | `string` | — |
1524
- | `text(name)` | `string` | — |
1525
- | `integer(name)` | `number` | — |
1526
- | `boolean(name)` / `boolean_(name)` | `boolean` | `_` suffix for JS reserved word |
1527
- | `timestamptz(name)` | `string` | — |
1528
- | `jsonb<T>(name)` | `T` | Generic for typed JSONB access |
1529
- | `textArray(name)` | `string[]` | TEXT[] |
1530
- | `vector(name, dims)` | `number[]` | pgvector support |
1531
-
1532
- **Column modifiers:** `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(val)`, `.unique()`, `.references(table, column?, onDelete?)`.
1533
-
1534
- **CRUD methods:**
1535
-
1536
- | Method | Description |
1537
- | ----------------------------- | ----------------------------------------------------------------- |
1538
- | `insert(data)` | INSERT + RETURNING \*, returns the inserted row |
1539
- | `insertMany(data)` | Bulk INSERT + RETURNING \*, returns rows |
1540
- | `read(id, opts?)` | SELECT by detected primary key + auto soft-delete filter |
1541
- | `readMany(where?, opts?)` | Filtered query with `{ count, data }` — auto-filters soft-deleted |
1542
- | `update(id, data)` | UPDATE by primary key + RETURNING \*, returns updated row |
1543
- | `updateMany(where, data)` | Bulk UPDATE, returns affected row count |
1544
- | `delete(id)` | Soft delete if `deleted_at` exists, else hard delete |
1545
- | `hardDelete(id)` | Always DELETE FROM |
1546
- | `deleteMany(where)` | Soft bulk delete if `deleted_at` exists |
1547
- | `hardDeleteMany(where)` | Always DELETE FROM |
1548
- | `upsert(data, conflict)` | INSERT ON CONFLICT DO UPDATE, returns row |
1549
- | `count(where?)` | SELECT COUNT(\*) — auto-filters soft-deleted |
1550
- | `create(opts?)` | CREATE TABLE IF NOT EXISTS |
1551
- | `drop(opts?)` | DROP TABLE IF EXISTS |
1552
- | `createIndex(columns, opts?)` | CREATE INDEX |
1553
- | `createUniqueIndex(columns)` | CREATE UNIQUE INDEX |
1554
- | `withSql(sql)` | Returns copy bound to a different sql (for transactions) |
1555
-
1556
- **Where helpers** — composable query conditions:
1557
-
1558
- | Helper | SQL |
1559
- | ----------------------------------- | ------------------------- |
1560
- | `eq(col, val)` | `"col" = val` |
1561
- | `ne(col, val)` | `"col" != val` |
1562
- | `gt` / `gte` / `lt` / `lte` | Comparison operators |
1563
- | `isNull(col)` / `isNotNull(col)` | `IS NULL` / `IS NOT NULL` |
1564
- | `like(col, pattern)` | `LIKE` |
1565
- | `contains(col, val)` | `@>` JSONB containment |
1566
- | `in_(col, vals)` | `= ANY(...)` |
1567
- | `and(...)` / `or(...)` / `not(...)` | Boolean composition |
1568
-
1569
- **PgModule** — base class for modules that need DB access:
1570
-
1571
- ```ts
1572
- class MyModule extends PgModule {
1573
- async migrate() {
1574
- /* run DDL */
1575
- }
1576
- async getUsers() {
1577
- return this.table('users', {}).readMany()
1578
- }
1579
- }
1580
- ```
1581
-
1582
- Where helpers + `and`/`or`/`not` can be imported from `'weifuwu'` alongside `postgres`. Full column builders and table helpers are in the same barrel.
1583
-
1584
- ### cron-utils [γ] [DevTools]
1585
-
1586
- Shared cron expression parsing utilities. All functions operate in **local timezone**.
1587
-
1588
- ```ts
1589
- import { cronNext } from 'weifuwu'
1590
-
1591
- // Next weekday at 09:00
1592
- const next = cronNext('0 9 * * 1-5')
1593
- console.log(new Date(next))
1594
- ```
1595
-
1596
- | Function | Description |
1597
- | ----------------------- | ---------------------------------------------------------- |
1598
- | `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]` |
1599
- | `matches(fields, date)` | Check if a date matches a parsed pattern |
1600
- | `cronNext(expr, from?)` | Calculate next matching timestamp (`from` defaults to now) |
1601
-
1602
- ### fts — Full-Text Search (PostgreSQL) [Database]
1603
-
1604
- Utilities for PostgreSQL full-text search: create GIN indexes, search with ranking, and generate highlighted snippets.
1605
-
1606
- ```ts
1607
- import { fts } from 'weifuwu'
1608
-
1609
- const articles = pg.table('articles', {
1610
- id: serial('id').primaryKey(),
1611
- title: text('title'),
1612
- body: text('body'),
1613
- })
1614
-
1615
- // Create search index
1616
- await fts.createIndex(pg.sql, articles, ['title', 'body'], { language: 'english' })
1617
-
1618
- // Search with ranking
1619
- const results = await fts.search(pg.sql, articles, 'node.js framework', {
1620
- fields: ['title', 'body'],
1621
- limit: 20,
1622
- headline: true, // highlighted snippets via ts_headline
1623
- })
1624
- // → [{ id, rank: 0.8, row: { title, body, ... }, headline: '...<b>Node.js</b> framework...' }]
1625
-
1626
- // Drop index
1627
- await fts.dropIndex(pg.sql, articles)
1628
- ```
1629
-
1630
- | Function | Description |
1631
- | ---------------------------------------- | ------------------------------ |
1632
- | `createIndex(sql, table, fields, opts?)` | Create GIN/GiST tsvector index |
1633
- | `search(sql, table, query, opts?)` | Search with ts_rank ordering |
1634
- | `dropIndex(sql, table, opts?)` | Drop the index |
1635
-
1636
- Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
1637
-
1638
- ### theme [α] [UX]
1639
-
1640
- ```ts
1641
- // Single line — auto-registers middleware + /__theme/:value route
1642
- app.use(theme({ default: 'dark' }))
1643
-
1644
- // ctx.theme = { value: 'dark', set: fn }
1645
- // ctx.theme.value — 'dark'
1646
- // ctx.theme.set('light', '/settings') — 302 + Set-Cookie
1647
-
1648
- // Explicit form for more control:
1649
- // const t = theme()
1650
- // app.use(t.middleware())
1651
- // app.use('/', t)
1652
- ```
1653
-
1654
- | Option | Type | Default | Description |
1655
- | --------- | -------- | ---------- | ------------------------------ |
1656
- | `default` | `string` | `'system'` | Default theme |
1657
- | `cookie` | `string` | `'theme'` | Cookie name (empty to disable) |
1658
-
1659
- ```ts
1660
- // Server-side switching
1661
- app.post('/settings', async (req, ctx) => {
1662
- const { theme } = await req.json()
1663
- return ctx.theme.set(theme, '/settings')
1664
- })
1665
- ```
1666
-
1667
- See [`useTheme()`](#usetheme) for client-side usage.
1668
-
1669
- ### i18n [α] [UX]
1670
-
1671
- ```ts
1672
- // Single line — auto-registers middleware + /__lang/:locale route
1673
- app.use(i18n({ default: 'zh', dir: './locales' }))
1674
-
1675
- // ctx.i18n = { locale: 'zh', t, set }
1676
- // ctx.i18n.t('welcome') → '欢迎'
1677
- // ctx.i18n.locale → 'zh'
1678
- // ctx.i18n.set('en', '/settings') — 302 + Set-Cookie
1679
-
1680
- // Explicit form for more control:
1681
- // const l = i18n()
1682
- // app.use(l.middleware())
1683
- // app.use('/', l)
1684
- ```
1685
-
1686
- | Option | Type | Default | Description |
1687
- | -------------------- | --------- | ---------- | -------------------------------------------------- |
1688
- | `default` | `string` | `'en'` | Default locale |
1689
- | `dir` | `string` | — | Directory with `{locale}.json` files |
1690
- | `messages` | `object` | — | Inline translations: `{ zh: { welcome: '欢迎' } }` |
1691
- | `cookie` | `string` | `'locale'` | Cookie name (empty to disable) |
1692
- | `fromAcceptLanguage` | `boolean` | `true` | Detect from Accept-Language header |
1693
-
1694
- ```ts
1695
- // Handler
1696
- app.get('/greet', async (req, ctx) => {
1697
- const greeting = ctx.i18n?.t('welcome', { name: 'Alice' })
1698
- return Response.json({ greeting, locale: ctx.i18n?.locale })
1699
- })
1700
- ```
1701
-
1702
- **Client-side:** import `useLocale` from `weifuwu/react`, `useTheme` from `weifuwu/react`.
1703
-
1704
- ### queue [α] [Database]
1705
-
1706
- Async job queue. Supports immediate, delayed, and recurring (cron) tasks with three backends:
1707
-
1708
- - `{ store: 'memory' }` — in-memory, zero dependency, suitable for dev & cron-like tasks
1709
- - `{ store: 'pg', pg }` — PostgreSQL-backed, persistent, multi-instance safe via `FOR UPDATE SKIP LOCKED`
1710
- - `{ store: 'redis', redis }` — Redis-backed, production-grade, distributed
1711
-
1712
- ```ts
1713
- // Create queue
1714
- const q = queue({ store: 'memory' })
1715
- // const q = queue({ store: 'pg', pg })
1716
- // const q = queue({ store: 'redis', redis })
1717
-
1718
- // Register cron job (uses the same backend for persistence)
1719
- q.cron('*/5 * * * *', async () => {
1720
- await cleanCache()
1721
- })
1722
-
1723
- // Or use process/add for full queue semantics
1724
- q.process('send-email', async (job) => {
1725
- await sendMail(job.payload)
1726
- })
1727
-
1728
- // Immediate
1729
- q.add('send-email', { to: 'user@test.com' })
1730
-
1731
- // Delayed
1732
- q.add('send-email', { to: 'user@test.com' }, { delay: 60_000 })
1733
-
1734
- // Scheduled (cron) — re-queues automatically
1735
- q.add('weekly-report', {}, { schedule: '0 9 * * 1' })
1736
-
1737
- q.run()
1738
- ```
1739
-
1740
- | Option | Type | Default | Description |
1741
- | -------------- | ----------------------------- | ---------- | ----------------------------------------------- |
1742
- | `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
1743
- | `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
1744
- | `url` | `string` | — | Redis URL (alternative to client) |
1745
- | `pg` | `object` | — | PostgreSQL client (required when `store: 'pg'`) |
1746
- | `prefix` | `string` | `'queue'` | Key/table prefix |
1747
- | `pollInterval` | `number` | `200` | Poll interval (ms) |
1748
-
1749
- | Method | Description |
1750
- | ---------------------------- | --------------------------------------------------- |
1751
- | `.cron(pattern, handler)` | Register a cron job (uses process + add internally) |
1752
- | `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`) |
1753
- | `.process(type, handler)` | Register job processor |
1754
- | `.run()` | Start processing |
1755
- | `.stop()` | Stop processing |
1756
- | `.jobs(limit?)` | List pending jobs |
1757
- | `.failedJobs(limit?)` | List failed jobs with error messages |
1758
- | `.retryFailed(jobId)` | Retry a specific failed job |
1759
- | `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
1760
- | `.dashboard()` | Returns a Router with management endpoints |
1761
- | `.close()` | Cleanup |
1762
-
1763
- **Schedule (cron) field reference:**
1764
-
1765
- | Field | Range |
1766
- | ------------ | -------------- |
1767
- | minute | 0–59 |
1768
- | hour | 0–23 |
1769
- | day of month | 1–31 |
1770
- | month | 1–12 |
1771
- | day of week | 0–6 (0=Sunday) |
1772
-
1773
- Supported cron syntax: `*` (any), `*/n` (every n), `n-m` (range), `n,m,o` (list), `n` (exact).
1774
-
1775
- **Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
1776
-
1777
- | Method | Path | Description |
1778
- | ------ | --------------- | ------------------------------------------- |
1779
- | GET | `/` | Queue stats + pending/failed counts by type |
1780
- | GET | `/:type/failed` | List failed jobs for a type |
1781
- | POST | `/:type/retry` | Retry all failed jobs of a type |
1782
- | POST | `/retry/:id` | Retry a specific failed job by ID |
1783
-
1784
- ### rateLimit [α] [Security]
1785
-
1786
- ```ts
1787
- app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
1788
- app.get('/api', rateLimit({ max: 10 }), handler) // per-route
1789
- app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
1790
-
1791
- // Multi-process: Redis-backed rate limiting
1792
- app.use(rateLimit({ max: 100, store: 'redis', redis: ctx.redis }))
1793
-
1794
- // Sets X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset, Retry-After headers
1795
- // m.stop() — clear interval (memory) or Redis cleanup
1796
- ```
1797
-
1798
- | Option | Type | Default | Description |
1799
- | --------- | --------------------- | --------------------- | --------------------------------------------- |
1800
- | `max` | `number` | `100` | Max requests per window |
1801
- | `window` | `number` | `60_000` | Window duration (ms) |
1802
- | `key` | `(req) => string` | IP-based | Key function |
1803
- | `message` | `string` | `'Too Many Requests'` | 429 response body |
1804
- | `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
1805
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1806
- | `prefix` | `string` | `'ratelimit:'` | Redis key prefix |
1807
-
1808
- Redis mode uses `INCR` + `EXPIRE` for atomic counting, enabling accurate rate limiting across multiple server processes. Memory mode is ideal for single-process deployments.
1809
-
1810
- ### redis [α] [Database]
1811
-
1812
- ```ts
1813
- const r = redis() // reads REDIS_URL
1814
- app.use(r) // injects ctx.redis
1815
- await ctx.redis.set('key', 'value')
1816
- // r.close() — cleanup
1817
- ```
1818
-
1819
- | Option | Type | Default | Description |
1820
- | --------------------- | -------- | --------------- | -------------------------- |
1821
- | `url` | `string` | `REDIS_URL` env | Redis connection string |
1822
- | (all ioredis options) | — | — | Passed directly to ioredis |
1823
-
1824
- ### requestId [α] [DevTools]
1825
-
1826
- ```ts
1827
- app.use(requestId())
1828
- app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID() }))
1829
- // Sets X-Request-ID header on responses, available as ctx.requestId
1830
- ```
1831
-
1832
- | Option | Type | Default | Description |
1833
- | ----------- | -------------- | --------------------- | ------------------------- |
1834
- | `header` | `string` | `'X-Request-ID'` | Header name to read/write |
1835
- | `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
1836
-
1837
- ### trace [α] [DevTools]
1838
-
1839
- Request-scoped tracing via `AsyncLocalStorage`. Use as middleware to inject `ctx.trace`:
1840
-
1841
- ```ts
1842
- import { trace } from 'weifuwu'
1843
- app.use(trace()) // → ctx.trace
1844
- app.use(trace({ header: 'X-Trace-Id' })) // custom header
1845
-
1846
- app.get('/', (req, ctx) => {
1847
- console.log(ctx.trace.requestId) // 550e8400-e29b-...
1848
- console.log(ctx.trace.traceId) // trace UUID
1849
- console.log(ctx.trace.elapsed()) // ms since request start
1850
- })
1851
- ```
1852
-
1853
- | Option | Type | Default | Description |
1854
- | ----------- | -------------- | --------------------- | ---------------------- |
1855
- | `header` | `string` | `'X-Request-ID'` | Request ID header name |
1856
- | `generator` | `() => string` | `crypto.randomUUID()` | Custom ID generator |
1857
-
1858
- Utility functions (also available standalone):
1859
-
1860
- ```ts
1861
- import { currentTraceId, runWithTrace, traceElapsed, currentTrace } from 'weifuwu'
1862
-
1863
- const traceId = currentTraceId() // UUID or incoming X-Trace-Id
1864
- const elapsed = traceElapsed() // ms since request started
1865
- runWithTrace(incomingId, () => { ... }) // manual scope
1866
- ```
1867
-
1868
- | Function | Description |
1869
- | --------------------------- | ---------------------------------------------------------- |
1870
- | `currentTraceId()` | Current request trace ID, or `undefined` outside a request |
1871
- | `currentTrace()` | Full `{ traceId, startTime }` context |
1872
- | `traceElapsed()` | Milliseconds elapsed since the trace started |
1873
- | `runWithTrace(traceId, fn)` | Execute `fn` inside a trace scope |
1874
-
1875
- ### s3 [α] — S3-compatible object storage [Networking]
1876
-
1877
- ```ts
1878
- import { s3 } from 'weifuwu'
1879
-
1880
- app.use(
1881
- s3({
1882
- bucket: 'my-app',
1883
- region: 'us-east-1',
1884
- endpoint: process.env.S3_URL, // MinIO / R2 / AWS
1885
- forcePathStyle: true, // required for MinIO
1886
- credentials: {
1887
- accessKeyId: process.env.S3_ACCESS_KEY,
1888
- secretAccessKey: process.env.S3_SECRET_KEY,
1889
- },
1890
- publicUrl: 'https://cdn.example.com', // for unsigned public URLs
1891
- }),
1892
- )
1893
- ```
1894
-
1895
- Injects `ctx.s3` with methods for S3-compatible object storage.
1896
-
1897
- ```ts
1898
- // Upload
1899
- await ctx.s3.put('images/logo.png', buffer, { contentType: 'image/png' })
1900
-
1901
- // Download
1902
- const buf = await ctx.s3.get('images/logo.png') // Buffer | null
1903
-
1904
- // Delete
1905
- await ctx.s3.delete('images/logo.png')
1906
-
1907
- // Check existence
1908
- if (await ctx.s3.exists('images/logo.png')) { ... }
1909
-
1910
- // Signed URL (expires in 1 hour by default)
1911
- const url = await ctx.s3.url('images/logo.png')
1912
- const shortUrl = await ctx.s3.url('images/logo.png', { expiresIn: 60 })
1913
-
1914
- // Public URL (requires publicUrl in options)
1915
- const publicUrl = await ctx.s3.url('images/logo.png', { expiresIn: 0 })
1916
-
1917
- // List objects under a prefix
1918
- const keys = await ctx.s3.list('images/')
1919
- ```
1920
-
1921
- | Option | Type | Default | Description |
1922
- | ---------------- | ---------------------------------- | ------------- | ------------------------------------------------------------------ |
1923
- | `bucket` | `string` | — | **Required.** S3 bucket name |
1924
- | `region` | `string` | `'us-east-1'` | AWS region |
1925
- | `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
1926
- | `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
1927
- | `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
1928
- | `publicUrl` | `string` | — | Base URL for unsigned public URLs via `url(key, { expiresIn: 0 })` |
1929
-
1930
- Credentials can be omitted to use AWS environment variables (`AWS_ACCESS_KEY_ID`,
1931
- `AWS_SECRET_ACCESS_KEY`) or IAM roles (EC2, ECS, Lambda).
1932
-
1933
- The module can also be used standalone without the middleware:
1934
-
1935
- ```ts
1936
- const storage = s3({ bucket: 'my-app', endpoint: '...' })
1937
- await storage.put('key', body)
1938
- const data = await storage.get('key')
1939
- ```
1940
-
1941
- Requires MinIO or another S3-compatible service for local development.
1942
- Add to `docker-compose.yml`:
1943
-
1944
- ```yml
1945
- minio:
1946
- image: minio/minio
1947
- ports:
1948
- - '9000:9000'
1949
- environment:
1950
- MINIO_ROOT_USER: minioadmin
1951
- MINIO_ROOT_PASSWORD: minioadmin
1952
- command: server /data
1953
- ```
1954
-
1955
- ### seo [β] + seoMiddleware [α] [API]
1956
-
1957
- ```ts
1958
- app.use(
1959
- '/',
1960
- seo({
1961
- baseUrl: 'https://example.com',
1962
- robots: [{ userAgent: '*', allow: '/' }],
1963
- sitemap: { urls: [{ loc: '/' }] },
1964
- }),
1965
- )
1966
- // GET /robots.txt, GET /sitemap.xml
1967
-
1968
- app.use(
1969
- seoMiddleware({
1970
- headers: { 'X-Robots-Tag': (path) => (path.startsWith('/admin') ? 'noindex' : undefined) },
1971
- }),
1972
- )
1973
- ```
1974
-
1975
- Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
1976
-
1977
- | Option | Type | Default | Description |
1978
- | --------- | ------------------ | ---------------------------------- | ----------------------------------------------- |
1979
- | `baseUrl` | `string` | — | Base URL for sitemap URLs |
1980
- | `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
1981
- | `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
1982
- | `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
1983
-
1984
- ### session [α] [Security]
1985
-
1986
- Cookie-based server-side session management with memory and Redis stores.
1987
-
1988
- ```ts
1989
- app.use(session()) // in-memory store (default)
1990
- app.use(session({ store: 'redis', redis: ctx.redis })) // Redis store
1991
- app.use(session({ store: 'redis', redis, ttl: 30 * 60_000, cookieName: 'sid' }))
1992
-
1993
- app.get('/login', async (req, ctx) => {
1994
- ctx.session.userId = 42
1995
- ctx.session.role = 'admin'
1996
- // Auto-saved on response — cookie set automatically
1997
- return Response.json({ ok: true })
1998
- })
1999
-
2000
- app.get('/logout', async (req, ctx) => {
2001
- ctx.session.destroy() // or ctx.session = null
2002
- return Response.json({ ok: true })
2003
- })
2004
-
2005
- // ctx.session.id — readonly session ID
2006
- // ctx.session.save() — explicit dirty mark (for deep mutations)
2007
- // ctx.session.destroy() — clear session + remove cookie
2008
- // Session mutations are auto-detected on property set/delete
2009
- ```
2010
-
2011
- | Option | Type | Default | Description |
2012
- | ----------------- | ------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- |
2013
- | `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
2014
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
2015
- | `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
2016
- | `cookieName` | `string` | `'__session'` | Cookie name |
2017
- | `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
2018
- | `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
2019
- | `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
2020
- | `cookie.path` | `string` | `'/'` | Cookie path |
2021
- | `cookie.domain` | `string` | — | Cookie domain |
2022
- | `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
2023
- | `rotateInterval` | `number` | `900000` (15min) | Auto-rotate session ID to prevent fixation attacks. Set `0` to disable |
2024
-
2025
- When `secret` is set, the cookie value is signed with HMAC-SHA256:
2026
- `uuid.base64url(hmac)`. Tampered cookies are rejected and treated as new
2027
- sessions (no error message, no data leak).
2028
-
2029
- Session ID auto-rotation copies data to a new ID and deletes the old one
2030
- from the store. Rotation happens transparently on the next request after
2031
- `rotateInterval` has elapsed.
2032
-
2033
- **Stores** are also exported for standalone use:
2034
-
2035
- ```ts
2036
- import { MemoryStore, RedisStore } from 'weifuwu'
2037
-
2038
- const mem = new MemoryStore() // auto-cleanup every 60s
2039
- await mem.set('sid', { userId: 1 }, 86400000)
2040
- mem.close()
2041
-
2042
- const redis = new RedisStore(redisClient, 'myapp:session:')
2043
- await redis.destroy('sid')
2044
- ```
2045
-
2046
- ### ssr({ dir }) [β] [SSR]
2047
-
2048
- One-stop Server-Side Rendering. Accepts a directory and returns a Router that handles all SSR routes, tailwind CSS, hydration, and livereload — using Next.js-style file conventions.
2049
-
2050
- ```ts
2051
- import { Router, ssr } from 'weifuwu'
2052
- const app = new Router()
2053
- app.use('/', ssr({ dir: './ui' }))
2054
- ```
2055
-
2056
- **Directory conventions (Next.js-style):**
2057
-
2058
- ```
2059
- ./ui/
2060
- ├── app/ ← only this directory affects routing
2061
- │ ├── globals.css ← tailwind CSS + CSS variables (optional)
2062
- │ ├── layout.tsx → root layout (wraps all pages)
2063
- │ ├── page.tsx → GET /
2064
- │ ├── not-found.tsx → 404 page (optional)
2065
- │ ├── error.tsx → error boundary (optional)
2066
- │ ├── about/
2067
- │ │ ├── page.tsx → GET /about
2068
- │ │ └── layout.tsx → group layout
2069
- │ └── posts/
2070
- │ ├── page.tsx → GET /posts
2071
- │ └── [id]/
2072
- │ └── page.tsx → GET /posts/:id
2073
- ├── components/ ← shared components (does not affect routing)
2074
- └── lib/ ← utilities (does not affect routing)
2075
- ```
2076
-
2077
- | Location | Route |
2078
- | ---------------------- | -------------------------------------------------------- |
2079
- | `app/page.tsx` | `GET /` |
2080
- | `app/[param]/page.tsx` | `GET /:param` |
2081
- | `app/layout.tsx` | Root layout (wraps all pages in its subtree) |
2082
- | `app/not-found.tsx` | 404 fallback for that subtree |
2083
- | `app/error.tsx` | Error boundary for that subtree |
2084
- | `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
2085
-
2086
- **How hydration works:**
2087
-
2088
- - Each page is lazy-resolved on first request — only the `page.tsx` and its layout chain are compiled
2089
- - An inline `<script type="module">` in the HTML handles hydration
2090
- - It imports `{ setCtx, TsxContext }` from the vendor bundle (`/__wfw/v/bundle`) via importmap
2091
- - Then dynamically imports the page component: `await import('/__ssr/[hash].js')`
2092
- - The vendor bundle (react + react-dom + weifuwu client libs) is compiled once and cached
2093
- - Page components are pre-compiled to `/__ssr/{hash}.js` — no runtime esbuild after first request
2094
- - **Dev:** `createRoot` + render; **Production:** `hydrateRoot` (reuses SSR DOM)
2095
- - The hydration script and page component share the same SSR context store — data flows seamlessly from server to client
2096
- - Tailwind CSS served at `/__wfw/style/{hash}.css` (cached, content-hashed)
2097
- - Dev mode extras: HMR WebSocket, file watcher, hot component replacement
2098
-
2099
- ```ts
2100
- // Multiple independent SSR directories
2101
- app.use('/', ssr({ dir: './www' }))
2102
- app.use('/admin', ssr({ dir: './admin' }))
2103
-
2104
- // API routes coexist normally
2105
- app.get('/api/ping', () => Response.json({ pong: true }))
2106
- ```
2107
-
2108
- Layout components receive `{ children }` and wrap from outer to inner:
2109
-
2110
- ### tenant [β] [Networking]
2111
-
2112
- Multi-tenant BaaS with dynamic table API and GraphQL.
2113
-
2114
- ```ts
2115
- const t = tenant({ pg, usersTable: '_users' })
2116
- await t.migrate()
2117
- app.use('/api', t.middleware()) // → ctx.tenant
2118
- app.use('/api', t) // dynamic CRUD
2119
- app.use('/graphql', t.graphql()) // dynamic GraphQL
2120
- ```
2121
-
2122
- | Option | Type | Default | Description |
2123
- | ------------ | -------- | ------- | --------------------------------------------- |
2124
- | `pg` | `object` | — | PostgreSQL client |
2125
- | `usersTable` | `string` | — | Users table name for tenant membership lookup |
2126
-
2127
- ### upload [α] [DevTools]
2128
-
2129
- ```ts
2130
- app.post(
2131
- '/upload',
2132
- upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }),
2133
- (req, ctx) => {
2134
- // ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
2135
- // Multiple files with same field name → array
2136
- // ctx.parsed.fields.title → 'hello'
2137
- },
2138
- )
2139
- ```
2140
-
2141
- | Option | Type | Default | Description |
2142
- | -------------- | ---------- | ------- | ---------------------------------------- |
2143
- | `dir` | `string` | — | Write files to disk (omit for in-memory) |
2144
- | `maxFileSize` | `number` | — | Max bytes per file |
2145
- | `allowedTypes` | `string[]` | — | Allowed MIME types |
2146
-
2147
- ### user [β] [Security]
2148
-
2149
- Authentication: register, login, JWT, OAuth2 服务端, 社会化登录.
2150
-
2151
- ```ts
2152
- const u = user({
2153
- pg,
2154
- jwtSecret: process.env.JWT_SECRET!,
2155
- oauthLogin: {
2156
- // 可选 — 社会化登录
2157
- providers: {
2158
- github: { clientId: '...', clientSecret: '...' },
2159
- google: { clientId: '...', clientSecret: '...' },
2160
- },
2161
- },
2162
- })
2163
- await u.migrate()
2164
- app.use(u) // POST /register, POST /login
2165
- app.use(u.middleware()) // ctx.user
2166
- // GET /auth/github, GET /auth/github/callback (如配置 oauthLogin)
2167
- ```
2168
-
2169
- | Option | Type | Default | Description |
2170
- | ------------ | -------- | ---------- | ------------------------------------------------------------------------------ |
2171
- | `pg` | `object` | — | PostgreSQL client |
2172
- | `jwtSecret` | `string` | — | JWT signing secret |
2173
- | `table` | `string` | `'_users'` | Users table name |
2174
- | `expiresIn` | `string` | `'24h'` | JWT expiration |
2175
- | `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
2176
- | `oauthLogin` | `object` | — | 社会化登录: `{ providers: Record<string, OAuthProviderConfig>, redirectUrl? }` |
2177
-
2178
- | Method | Description |
2179
- | ----------------- | --------------------------------------- |
2180
- | `.register(data)` | Register a new user programmatically |
2181
- | `.login(data)` | Log in programmatically |
2182
- | `.verify(token)` | Verify JWT token |
2183
- | `.middleware()` | JWT verify middleware — sets `ctx.user` |
2184
-
2185
- **API Key management** — enable via `user({ apiKeys: true })`:
2186
-
2187
- ```ts
2188
- const auth = user({ pg, jwtSecret: process.env.JWT_SECRET, apiKeys: true })
2189
- await auth.migrate()
2190
- app.use(auth)
2191
-
2192
- // Create an API key (server-side)
2193
- const { id, key } = await auth.createApiKey(userId, 'Deploy Key', ['read', 'deploy'])
2194
- // key → 'sk_live_abc123...' (only shown once!)
2195
-
2196
- // List keys (masked)
2197
- const keys = await auth.listApiKeys(userId)
2198
- // → [{ id, name, prefix: 'sk_live_abc...f3a2', scopes: ['read','deploy'], last_used_at, revoked }]
2199
-
2200
- // Revoke a key
2201
- await auth.revokeApiKey(userId, keyId)
2202
-
2203
- // REST API (auto-mounted when routes are registered)
2204
- // POST /api-keys → Create (requires auth)
2205
- // GET /api-keys → List (requires auth)
2206
- // DELETE /api-keys/:id → Revoke (requires auth)
2207
- ```
2208
-
2209
- API keys start with `sk_live_` and are hashed with SHA256 before storage.
2210
- The middleware resolves API keys automatically — use them with `Authorization: Bearer sk_live_...`.
2211
-
2212
- _New in v0.25._
2213
-
2214
- ### permissions [α] — RBAC [Security]
2215
-
2216
- Role-based access control.
2217
-
2218
- ```ts
2219
- const perm = permissions({ pg })
2220
- await perm.migrate()
2221
-
2222
- // Assign roles & permissions
2223
- await perm.assignRole(userId, 'admin')
2224
- await perm.grantPermission('admin', 'posts:create')
2225
- await perm.grantPermission('admin', 'posts:edit')
2226
- await perm.grantPermission('admin', '*') // wildcard — all permissions
2227
-
2228
- // Use as middleware
2229
- app.use((req, ctx, next) => {
2230
- ctx.user = { id: userId }
2231
- return next(req, ctx)
2232
- })
2233
- app.use(perm) // → ctx.roles, ctx.permissions
2234
-
2235
- // Route guards
2236
- app.get('/admin', perm.requireRole('admin'), adminHandler)
2237
- app.post('/posts', perm.requirePermission('posts:create'), createHandler)
2238
-
2239
- // Handler-level check
2240
- app.get('/posts/:id', async (req, ctx) => {
2241
- if (!ctx.permissions.has('posts:read')) {
2242
- return Response.json({ error: 'forbidden' }, { status: 403 })
2243
- }
2244
- return Response.json(post)
2245
- })
2246
- ```
2247
-
2248
- | Option | Type | Default | Description |
2249
- | -------- | -------- | ------- | --------------------------------------------- |
2250
- | `pg` | `object` | — | PostgreSQL client |
2251
- | `prefix` | `string` | `''` | Table prefix (e.g. `'myapp'` → `myapp_roles`) |
2252
-
2253
- | Method | Description |
2254
- | ------------------------------------- | --------------------------------------------------- |
2255
- | `.assignRole(userId, role)` | Assign role to user (creates role if missing) |
2256
- | `.removeRole(userId, role)` | Remove role from user |
2257
- | `.grantPermission(role, permission)` | Grant permission to role |
2258
- | `.revokePermission(role, permission)` | Revoke permission from role |
2259
- | `.getUserRoles(userId)` | List user's roles |
2260
- | `.getUserPermissions(userId)` | List user's permissions (union of all roles) |
2261
- | `.requireRole(...roles)` | Middleware — rejects if user lacks any of the roles |
2262
- | `.requirePermission(...perms)` | Middleware — rejects if user lacks any permission |
2263
- | `.migrate()` | Create tables |
2264
-
2265
- ### validate [α] [DevTools]
2266
-
2267
- ````ts
2268
- import { z } from 'zod'
2269
- const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
2270
- app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string().optional() }) }), (req, ctx) => {
2271
- // ctx.parsed.body — typed & validated
2272
- // ctx.parsed.query — typed & validated
2273
- // ctx.parsed.params — typed & validated (for dynamic routes)
2274
- // ctx.parsed.headers — typed & validated
2275
- })
2276
- // Validation failure: returns 400 with { error: 'Validation failed', issues: [...] }
2277
-
2278
- **Form body auto-parsing** — `application/x-www-form-urlencoded` bodies are automatically parsed into `Record<string, string>` via `URLSearchParams`, even without a Zod schema:
2279
-
2280
- ```ts
2281
- // No schema needed — just parse the form
2282
- app.post('/contact', validate(), (req, ctx) => {
2283
- const email = ctx.parsed.body.email // string
2284
- const msg = ctx.parsed.body.message // string
2285
- return Response.json({ received: true })
2286
- })
2287
-
2288
- // Or validate with Zod
2289
- app.post('/contact', validate({ body: z.object({ email: z.string().email() }) }), handler)
2290
- ````
2291
-
2292
- | Option | Type | Default | Description |
2293
- | --------- | ----------- | ------- | ------------------------------------- |
2294
- | `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
2295
- | `query` | `ZodSchema` | — | Query validation schema |
2296
- | `params` | `ZodSchema` | — | URL params validation schema |
2297
- | `headers` | `ZodSchema` | — | Header validation schema |
2298
-
2299
- ### webhook [β] [API]
2300
-
2301
- Webhook receiver with built-in signature verification for Stripe, GitHub, and Slack. Event-based dispatch with replay protection.
2302
-
2303
- ```ts
2304
- import { webhook } from 'weifuwu'
2305
-
2306
- const wh = webhook({
2307
- stripe: { secret: process.env.STRIPE_WEBHOOK_SECRET! },
2308
- github: { secret: process.env.GITHUB_WEBHOOK_SECRET! },
2309
- slack: { secret: process.env.SLACK_WEBHOOK_SECRET! },
2310
- })
2311
-
2312
- app.use('/webhooks', wh)
2313
-
2314
- wh.on('checkout.session.completed', async (event, ctx) => {
2315
- await fulfillOrder(event.payload.data.object)
2316
- })
2317
-
2318
- wh.on('push', async (event, ctx) => {
2319
- await triggerCI(event.payload)
2320
- })
2321
-
2322
- wh.on('*', (event) => {
2323
- console.log(`Received ${event.provider}.${event.event}`)
2324
- })
2325
- ```
2326
-
2327
- | Option | Type | Default | Description |
2328
- | ------------------ | ------------------------ | --------- | ----------------------------------- |
2329
- | `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
2330
- | `github` | `PlatformConfig` | — | GitHub webhook config |
2331
- | `slack` | `PlatformConfig` | — | Slack webhook config |
2332
- | `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
2333
- | `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
2334
- | `idempotencyTTL` | `number` | `3600000` | Dedup TTL (ms) |
2335
-
2336
- Built-in verifiers handle HMAC-SHA256, timestamp validation (Slack's 5-min window), and Stripe's `t=` / `v1=` signature format. Slack URL verification challenges are auto-responded.
2337
-
2338
- ### Client-side navigation [δ] [Client]
2339
-
2340
- ```tsx
2341
- import { Link, navigate, useNavigate, useNavigating } from 'weifuwu/react'
2342
- ;<Link href="/about" prefetch>
2343
- About
2344
- </Link> // client-side nav + prefetch on hover/visible
2345
- const n = useNavigate() // hook: n('/contact')
2346
- navigate('/contact') // bare function (no hook needed)
2347
- const loading = useNavigating() // reactive loading state
2348
- ```
2349
-
2350
- `navigate()` fetches the SSR page, extracts the root container content, and replaces it in-place. Middleware runs on server each nav — data is always fresh.
2351
-
2352
- **Preference URLs** (`/__lang/`, `/__theme/`) are intercepted by modular interceptors registered via `addInterceptor()` — no page reload needed. Importing `useLocale` or `useTheme` registers the interceptor automatically.
2353
-
2354
- ### Client-side hooks [δ] [Client]
2355
-
2356
- ```tsx
2357
- import { useWebsocket, useAction, useFetch, useQueryState, createStore, Head } from 'weifuwu/react'
2358
- import { useLocale, useTheme, applyTheme, addInterceptor, useLoaderData, useFlashMessage } from 'weifuwu/react'
2359
-
2360
- // WebSocket — auto-reconnecting
2361
- const { send, lastMessage, readyState, close, reconnect } = useWebsocket('/ws/chat', {
2362
- onMessage: (d) => console.log(d),
2363
- reconnect: { maxRetries: 10, delay: 3000 },
2364
- protocols: [], // optional sub-protocols
2365
- enabled: true, // pause/resume connection
2366
- })
2367
-
2368
- // Form action
2369
- const { submit, data, error, pending, reset } = useAction('/api/feedback', {
2370
- method: 'POST',
2371
- headers: { 'X-Custom': 'value' },
2372
- onSuccess: (data) => console.log(data),
2373
- onError: (err) => console.error(err),
2374
- })
2375
- // Auto-reads _csrf cookie, sends as x-csrf-token or x-xsrf-token
2376
-
2377
- // Data fetching — cache + dedup + mutate
2378
- const { data, error, loading, mutate } = useFetch('/api/posts', { fallback: loadData, ttl: 30_000 })
2379
-
2380
- // URL query state
2381
- const [q, setQ] = useQueryState('q', '')
2382
- const [page, setPage] = useQueryState('page', '1')
2383
-
2384
- // Shared state — persists across client navs
2385
- const useStore = createStore({ count: 0 })
2386
- const count = useStore(s => s.count)
2387
-
2388
- // Per-page meta tags
2389
- <Head><title>Page Title</title><meta name="description" content="..." /></Head>
2390
- ```
2391
-
2392
- **`TsxContext`** — React context holding page data (`params`, `query`, `user`, `parsed`, `theme`, `i18n`, `flash`, `loaderData`, `env`). Used internally by hooks; rarely needed directly.
2393
-
2394
- ### Locale & Theme [δ] [Client]
2395
-
2396
- ```tsx
2397
- import { useLocale } from 'weifuwu/react'
2398
- function LangSwitch() {
2399
- const { locale, setLocale, t } = useLocale()
2400
- return <button onClick={() => setLocale('zh-CN')}>{t('switch_lang')}</button>
2401
- }
2402
- ```
2403
-
2404
- | Return | Description |
2405
- | ------------------- | ----------------------------------------------------- |
2406
- | `locale` | Current locale string (from `ctx.i18n.locale`) |
2407
- | `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
2408
- | `t` | Translate a key using loaded locale messages |
2409
-
2410
- ```tsx
2411
- import { useTheme } from 'weifuwu/react'
2412
- function ThemeToggle() {
2413
- const { theme, resolvedTheme, setTheme } = useTheme()
2414
- return (
2415
- <>
2416
- <span>Current: {resolvedTheme}</span> {/* 'dark' | 'light' — never 'system' */}
2417
- <select value={theme} onChange={(e) => setTheme(e.target.value)}>
2418
- <option value="light">☀ Light</option>
2419
- <option value="dark">🌙 Dark</option>
2420
- <option value="system">💻 System</option>
2421
- </select>
2422
- </>
2423
- )
2424
- }
2425
- ```
2426
-
2427
- | Return | Description |
2428
- | ----------------- | ---------------------------------------------------------------- |
2429
- | `theme` | Raw preference (`'light'` \| `'dark'` \| `'system'`) |
2430
- | `resolvedTheme` | Resolved value (`'light'` \| `'dark'`) — `'system'` → matchMedia |
2431
- | `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`) |
2432
-
2433
- **`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
2434
-
2435
- **`useLoaderData()`** — Returns middleware-injected data from the request context. Works identically on server (SSR) and client (hydration/SPA). Re-renders on SPA navigation.
2436
-
2437
- ```tsx
2438
- import { useLoaderData } from 'weifuwu/react'
2439
- function Page() {
2440
- const data = useLoaderData<{ posts: Post[] }>()
2441
- return (
2442
- <ul>
2443
- {data.posts.map((p) => (
2444
- <li key={p.id}>{p.title}</li>
2445
- ))}
2446
- </ul>
2447
- )
2448
- }
2449
- ```
2450
-
2451
- On the server, data flows from middleware → `ctx` → `setCtx(ctxValue)` (serialized via JSON). On the client, the hydration script calls `setCtx(ctxData)` which populates the shared context store. `useLoaderData()` reads from the snapshot via `useSyncExternalStore` — no SSR-specific code needed in your components.
2452
-
2453
- **`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
2454
-
2455
- ```ts
2456
- import { addInterceptor } from 'weifuwu/react'
2457
- addInterceptor(async (url) => {
2458
- if (!url.pathname.startsWith('/__custom/')) return false
2459
- // handle without page reload
2460
- return true
2461
- })
2462
- ```
2463
-
2464
- ### Flash messages [δ] [Client]
2465
-
2466
- ```ts
2467
- import { flash } from 'weifuwu'
2468
-
2469
- app.use(flash())
2470
-
2471
- // Read flash
2472
- app.get('/', (req, ctx) => {
2473
- const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
2474
- })
2475
-
2476
- // Set flash + redirect
2477
- app.post('/save', async (req, ctx) => {
2478
- await saveArticle()
2479
- return ctx.flash.set({ type: 'success', text: '已保存' }, '/articles')
2480
- // → 302 /articles + Set-Cookie flash=...
2481
- })
2482
- ```
2483
-
2484
- ```tsx
2485
- // Client
2486
- import { useFlashMessage } from 'weifuwu/react'
2487
-
2488
- function Toast() {
2489
- const flash = useFlashMessage<{ type: string; text: string }>()
2490
- if (!flash) return null
2491
- return <div className={`toast toast-${flash.type}`}>{flash.text}</div>
2492
- }
2493
- ```
2494
-
2495
- | Option | Type | Default | Description |
2496
- | ------ | -------- | --------- | ----------- |
2497
- | `name` | `string` | `'flash'` | Cookie name |
2498
-
2499
- ### Dev mode [δ] [Client]
2500
-
2501
- Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically registers importmap, vendor bundle, HMR WebSocket, and file watcher. No explicit setup needed.
2502
-
2503
- - Inline hydration script uses `createRoot` + render (replaces SSR DOM)
2504
- - Vendor bundle served at `/__wfw/v/bundle?h=<hash>` — compiled from source, unminified
2505
- - Hot component replacement: file changes → WebSocket message → browser imports hot bundle → component refreshed in place — `useState` values preserved
2506
- - Tailwind CSS hot-reloads without page refresh
2507
- - Layout changes trigger a full page reload
2508
-
2509
- ---
2510
-
2511
- ## AI
2512
-
2513
- ```ts
2514
- import {
2515
- openai,
2516
- streamText,
2517
- generateText,
2518
- streamObject,
2519
- generateObject,
2520
- tool,
2521
- embed,
2522
- embedMany,
2523
- aiProvider,
2524
- } from 'weifuwu'
2525
- import { runWorkflow } from 'weifuwu'
2526
-
2527
- const provider = aiProvider()
2528
- ```
2529
-
2530
- For AI streaming endpoints see [`aiStream`](#aistream-β). For AI agent APIs see [`agent`](#agent-β).
2531
-
2532
- ### aiProvider [α] — AI model & embedding configuration [AI]
2533
-
2534
- ```ts
2535
- const provider = aiProvider() // auto from env
2536
- app.use(provider) // → ctx.ai
2537
-
2538
- // Handler
2539
- app.post('/ask', async (req, ctx) => {
2540
- const { text } = await ctx.ai.generateText({ prompt: 'hello' })
2541
- const vec = await ctx.ai.embed('some text')
2542
- const stream = ctx.ai.streamText({ system: 'assistant', messages: [...] })
2543
- return stream.toTextStreamResponse()
2544
- })
2545
- ```
2546
-
2547
- | Option | Type | Default | Description |
2548
- | -------------------- | -------- | -------------------------------------------------------- | -------------------- |
2549
- | `baseURL` | `string` | `OPENAI_BASE_URL` env or `http://localhost:11434/v1` | API base URL |
2550
- | `apiKey` | `string` | `OPENAI_API_KEY` env or `'ollama'` | API key |
2551
- | `model` | `string` | `OPENAI_MODEL` env or `'qwen3:0.6b'` | Chat model name |
2552
- | `embeddingModel` | `string` | `OPENAI_EMBEDDING_MODEL` env or `'qwen3-embedding:0.6b'` | Embedding model name |
2553
- | `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024` | Vector dimension |
2554
-
2555
- | Method | Description |
2556
- | ------------------------ | --------------------------------------- |
2557
- | `.model(name?)` | Get `LanguageModel` instance |
2558
- | `.embeddingModel(name?)` | Get `EmbeddingModel` instance |
2559
- | `.embed(text)` | Embed single text → `Promise<number[]>` |
2560
- | `.embedMany(texts)` | Batch embed → `Promise<number[][]>` |
2561
- | `.generateText(params)` | Generate text (model auto-injected) |
2562
- | `.streamText(params)` | Stream text (model auto-injected) |
2563
- | `.dimension` | Configured embedding dimension |
2564
-
2565
- ### DAG Workflow [AI]
2566
-
2567
- ```ts
2568
- const tools = { queryUser: tool({ ... }) }
2569
-
2570
- // Via provider:
2571
- const wf = runWorkflow({ tools, provider })
2572
-
2573
- // Or explicit model:
2574
- const wf = runWorkflow({ tools, model: openai('gpt-4o') })
2575
- ```
2576
-
2577
- | Option | Type | Default | Description |
2578
- | ---------- | --------------- | ------- | ---------------------------------------------------------------- |
2579
- | `tools` | `object` | — | Registered tool definitions |
2580
- | `provider` | `AIProvider` | — | AI provider (uses `provider.model()` for LLM-generated workflow) |
2581
- | `model` | `LanguageModel` | — | Explicit model (overrides provider) |
2582
- | `maxSteps` | `number` | `200` | Max execution steps |
2583
-
2584
- ---
2585
-
2586
- ## Server-Sent Events
2587
-
2588
- ```ts
2589
- import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
2590
- async function* events() {
2591
- yield formatSSE('chat', 'Hello')
2592
- yield formatSSE('chat', 'World')
2593
- }
2594
- app.get('/stream', (req, ctx) => createSSEStream(events()))
2595
- ```
2596
-
2597
- ---
2598
-
2599
- ## Complete export index
2600
-
2601
- Every public symbol can be imported from `'weifuwu'`:
2602
-
2603
- ### Core
2604
-
2605
- ```ts
2606
- serve, createTestServer, Router, ssr,
2607
- Context, Handler, Middleware, ErrorHandler, ServeOptions, Server,
2608
- loadEnv, env, isDev, isProd, isBundled, getPublicEnv,
2609
- currentTraceId, currentTrace, runWithTrace, traceElapsed, trace, TraceContext,
2610
- testApp, TestApp, TestRequest, TestResponse,
2611
- createTestDb, withTestDb,
2612
- getCookies, setCookie, deleteCookie,
2613
- createSSEStream, formatSSE, formatSSEData, SSEEvent,
2614
- DEFAULT_MAX_BODY, MIGRATIONS_TABLE,
2615
- ```
2616
-
2617
- ### Middleware / DevTools
2618
-
2619
- ```ts
2620
- logger, cors, compress, helmet,
2621
- rateLimit, requestId, validate, upload,
2622
- csrf, session, MemoryStore, RedisStore, SessionStore,
2623
- cache, MemoryCache, RedisCache, CacheStore,
2624
- flash, permissions,
2625
- serveStatic, s3,
2626
- ```
2627
-
2628
- ### Database
2629
-
2630
- ```ts
2631
- postgres, PostgresOptions, PostgresClient,
2632
- redis, RedisOptions, RedisClient,
2633
- queue, QueueOptions, QueueJob, Queue,
2634
- PostgresInjected, RedisInjected, QueueInjected,
2635
- // Schema helpers:
2636
- pgTable, SQL, sql,
2637
- ColumnBuilder, serial, uuid, text, integer, boolean, boolean_, timestamptz, jsonb, textArray, vector,
2638
- partitionBy, timestamps, toDDL, PartitionByDef,
2639
- Table, BoundTable, IndexOptions, FindOptions, CreateOptions,
2640
- eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not,
2641
- fts,
2642
- ```
2643
-
2644
- ### Security / Auth
2645
-
2646
- ```ts
2647
- auth,
2648
- user, UserModule, UserData, UserOptions, UserInjected, OAuthProviderConfig, OAuth2Client,
2649
- permissions, PermissionsModule, PermissionsOptions,
2650
- csrf, CsrfOptions, CsrfInjected,
2651
- helmet, HelmetOptions,
2652
- session, SessionStore, SessionOptions, SessionData, SessionInjected,
2653
- rateLimit, RateLimitOptions,
2654
- ```
2655
-
2656
- ### UX Middleware
2657
-
2658
- ```ts
2659
- theme, ThemeOptions, ThemeInjected,
2660
- i18n, I18nOptions, I18nInjected,
2661
- flash, FlashOptions, FlashInjected,
2662
- ```
2663
-
2664
- ### AI
2665
-
2666
- ```ts
2667
- aiProvider, AIProvider, AIProviderOptions, AIProviderInjected,
2668
- streamText, generateText, streamObject, generateObject,
2669
- tool, embed, embedMany, smoothStream,
2670
- openai, createOpenAI,
2671
- aiStream, AIHandler,
2672
- runWorkflow,
2673
- agent, AgentModule, AgentOptions,
2674
- knowledgeBase, KBModule, KBOptions,
2675
- opencode, OpencodeModule, OpencodeOptions,
2676
- ```
2677
-
2678
- ### API / Routing
2679
-
2680
- ```ts
2681
- analytics, AnalyticsModule, AnalyticsOptions,
2682
- health, HealthOptions,
2683
- graphql, GraphQLOptions, GraphQLHandler,
2684
- logdb, LogdbModule, LogdbOptions,
2685
- seo, seoMiddleware, seoTags, SeoOptions,
2686
- webhook, WebhookModule, WebhookOptions,
2687
- iii, createWorker, registerWorker, IIIModule, IIIOptions,
2688
- ```
2689
-
2690
- ### Networking / Storage
2691
-
2692
- ```ts
2693
- s3, S3Options, S3Module, S3Body,
2694
- mailer, MailerOptions, Mailer,
2695
- messager, MessagerModule, MessagerOptions,
2696
- hub, createHub, Hub, HubOptions,
2697
- deploy, defineConfig, DeployConfig, AppConfig,
2698
- tenant, TenantModule, TenantOptions, TenantContext,
2699
- ```
2700
-
2701
- ### Client-side (from `'weifuwu/react'`)
2702
-
2703
- ```ts
2704
- TsxContext, setCtx, useCtx, addCtxRebuilder, useLoaderData,
2705
- useWebsocket, useAction, useFetch, useQueryState, createStore,
2706
- Link, useNavigate, useNavigating, addInterceptor,
2707
- useLocale, useTheme, applyTheme, useFlashMessage,
2708
- useAgentStream,
2709
- Head,
2710
-
2711
- // Types:
2712
- StoreApi,
2713
- UseActionOptions, UseActionReturn,
2714
- UseWebsocketOptions, UseWebsocketReturn,
2715
- UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState,
2716
- ```
2717
-
2718
- ---
514
+ ## Dependencies
2719
515
 
2720
- ## License
516
+ - `postgres` — PostgreSQL client
517
+ - `ioredis` — Redis client
518
+ - `ai`, `@ai-sdk/openai` — AI SDK
519
+ - `graphql`, `@graphql-tools/schema` — GraphQL
520
+ - `ws` — WebSocket
521
+ - `zod` — Schema validation
522
+ - `nodemailer` — Email
2721
523
 
2722
- MIT
524
+ Zero build tools. Zero frontend framework dependencies.