weifuwu 0.24.1 → 0.24.2

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 (54) hide show
  1. package/README.md +818 -712
  2. package/cli/template/app.ts +5 -1
  3. package/cli/template/index.ts +4 -1
  4. package/cli/template/locales/en.json +6 -1
  5. package/cli/template/locales/zh-CN.json +6 -1
  6. package/cli/template/locales/zh-TW.json +6 -1
  7. package/cli/template/locales/zh.json +6 -1
  8. package/cli/template/ui/app/globals.css +1 -1
  9. package/cli/template/ui/app/page.tsx +55 -16
  10. package/cli.ts +148 -104
  11. package/dist/agent/rest.d.ts +1 -1
  12. package/dist/agent/run.d.ts +2 -2
  13. package/dist/ai/workflow.d.ts +1 -1
  14. package/dist/ai-sdk.d.ts +1 -1
  15. package/dist/cli.js +135 -97
  16. package/dist/cookie.d.ts +24 -0
  17. package/dist/fts.d.ts +5 -5
  18. package/dist/iii/index.d.ts +1 -1
  19. package/dist/index.d.ts +5 -5
  20. package/dist/index.js +787 -346
  21. package/dist/live.d.ts +2 -3
  22. package/dist/logdb/rest.d.ts +1 -1
  23. package/dist/mailer.d.ts +1 -1
  24. package/dist/messager/agent.d.ts +2 -2
  25. package/dist/messager/rest.d.ts +3 -3
  26. package/dist/messager/ws.d.ts +3 -3
  27. package/dist/opencode/index.d.ts +1 -1
  28. package/dist/opencode/permissions.d.ts +1 -1
  29. package/dist/opencode/run.d.ts +1 -1
  30. package/dist/opencode/session.d.ts +9 -9
  31. package/dist/opencode/tools/web.d.ts +1 -1
  32. package/dist/opencode/ws.d.ts +1 -2
  33. package/dist/permissions.d.ts +2 -2
  34. package/dist/postgres/module.d.ts +3 -3
  35. package/dist/postgres/schema/index.d.ts +1 -1
  36. package/dist/postgres/schema/table.d.ts +22 -20
  37. package/dist/postgres/types.d.ts +4 -4
  38. package/dist/queue/types.d.ts +1 -1
  39. package/dist/react.d.ts +1 -1
  40. package/dist/react.js +135 -90
  41. package/dist/router.d.ts +10 -10
  42. package/dist/session.d.ts +1 -2
  43. package/dist/tenant/graphql.d.ts +2 -2
  44. package/dist/tenant/index.d.ts +1 -1
  45. package/dist/tenant/rest.d.ts +2 -2
  46. package/dist/test-utils.d.ts +3 -3
  47. package/dist/user/index.d.ts +1 -1
  48. package/dist/user/oauth-login.d.ts +2 -2
  49. package/dist/vendor.d.ts +4 -0
  50. package/opencode/ui/app/globals.css +1 -1
  51. package/opencode/ui/app/layout.tsx +2 -3
  52. package/opencode/ui/app/page.tsx +302 -73
  53. package/package.json +26 -3
  54. package/cli/template/.weifuwu/ssr/2e3a7e60.js +0 -112
package/README.md CHANGED
@@ -30,7 +30,20 @@ npx weifuwu init my-app && cd my-app && npm run dev
30
30
  ### Typical Full App
31
31
 
32
32
  ```ts
33
- import { serve, Router, postgres, session, user, aiProvider, ssr, flash, i18n, theme, logger, rateLimit } from 'weifuwu'
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'
34
47
 
35
48
  const app = new Router()
36
49
 
@@ -50,14 +63,14 @@ app.use(pg)
50
63
  app.use(session({ store: 'redis', redis: myRedis }))
51
64
  const auth = user({ pg, jwtSecret: process.env.JWT_SECRET })
52
65
  await auth.migrate()
53
- app.use(auth) // auto-registers middleware + /register, /login
54
- app.use('/auth', auth) // explicit path mounts for more control
66
+ app.use(auth) // auto-registers middleware + /register, /login
67
+ app.use('/auth', auth) // explicit path mounts for more control
55
68
 
56
69
  // 5. API protection
57
70
  app.use('/api', rateLimit({ max: 60, window: 60_000 }))
58
71
 
59
72
  // 6. AI
60
- app.use(aiProvider()) // ctx.ai
73
+ app.use(aiProvider()) // ctx.ai
61
74
 
62
75
  // 7. SSR
63
76
  app.use('/', ssr({ dir: './ui' }))
@@ -94,21 +107,21 @@ const server = serve(handler, { port: 3000 })
94
107
  await server.ready
95
108
  ```
96
109
 
97
- | Option | Type | Default | Description |
98
- |--------|------|---------|-------------|
99
- | `port` | `number` | `0` | Listen port |
100
- | `hostname` | `string` | `'0.0.0.0'` | Listen address |
101
- | `signal` | `AbortSignal` | — | Shutdown on abort |
102
- | `websocket` | `WsUpgradeHandler` | — | WebSocket upgrade handler |
103
- | `maxBodySize` | `number` | `10MB` | Max body bytes (0 = unlimited) |
104
- | `timeout` | `number` | `30_000` | Socket inactivity timeout (ms) |
105
- | `keepAliveTimeout` | `number` | `5_000` | Keep-Alive idle timeout (ms) |
106
- | `headersTimeout` | `number` | `6_000` | Headers read timeout (ms) |
107
- | `shutdown` | `boolean` | `true` | Auto SIGTERM/SIGINT |
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 |
108
121
 
109
122
  ```ts
110
123
  interface Server {
111
- stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
124
+ stop: (timeoutMs?: number) => Promise<void> // graceful: waits for in-flight, force-closes after timeoutMs (default 10s)
112
125
  readonly port: number
113
126
  readonly hostname: string
114
127
  ready: Promise<void>
@@ -126,16 +139,27 @@ are forcibly closed. SIGTERM/SIGINT use the same graceful pattern.
126
139
  ```ts
127
140
  const app = new Router()
128
141
  app.get('/hello/:name', (req, ctx) => Response.json({ message: `Hello, ${ctx.params.name}!` }))
129
- app.post('/data', async (req, ctx) => { const body = await req.json(); return Response.json(body, { status: 201 }) })
130
- app.use('/admin', authMW) // path-scoped middleware
131
- app.use('/admin', adminRouter) // sub-router (flattened into parent trie)
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)
132
148
  app.ws('/echo', {
133
- open(ws, ctx) { ctx.ws.json({ type: 'connected' }) },
134
- message(ws, ctx, data) { ctx.ws.json({ echo: data.toString() }) },
149
+ open(ws, ctx) {
150
+ ctx.ws.json({ type: 'connected' })
151
+ },
152
+ message(ws, ctx, data) {
153
+ ctx.ws.json({ echo: data.toString() })
154
+ },
135
155
  })
136
156
  app.ws('/chat', {
137
- open(ws, ctx) { ctx.ws.join('room') },
138
- message(ws, ctx, data) { ctx.ws.sendRoom('room', JSON.parse(data.toString())) },
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
+ },
139
163
  })
140
164
  app.onError((err, req, ctx) => Response.json({ error: err.message }, { status: 500 }))
141
165
 
@@ -152,11 +176,11 @@ const wsHandler = app.websocketHandler()
152
176
  serve(handler, { port: 3000, websocket: wsHandler })
153
177
  ```
154
178
 
155
- | Pattern | Example | Match |
156
- |---------|---------|-------|
157
- | Static | `/about` | exact |
158
- | Param | `/users/:id` | `/users/42` → `ctx.params.id` |
159
- | Wildcard | `/static/*` | `/static/js/app.js` |
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` |
160
184
 
161
185
  Query params → `ctx.query`.
162
186
 
@@ -170,7 +194,7 @@ Request → serve() → app.handler() → global middleware × N → path middle
170
194
 
171
195
  1. `serve()` receives HTTP request
172
196
  2. `app.handler()` creates `ctx = { params, query }` and routes to the matching trie node
173
- 3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
197
+ 3. **Global middleware** runs in `use()` order (e.g. `theme()`, `i18n()`, `postgres()`, `cors()`)
174
198
  4. **Path‑scoped middleware** runs for matching paths (e.g. `app.use('/admin', authMW)`)
175
199
  5. **Route‑level middleware** runs (e.g. `app.get('/admin', validate(...), handler)`)
176
200
  6. **Route handler** returns `Response` — middleware chain unwinds
@@ -181,40 +205,40 @@ Sub-routers (`app.use('/admin', adminRouter)`) are **flattened** into the parent
181
205
 
182
206
  ```ts
183
207
  type Middleware = (req: Request, ctx: Context, next: Handler) => Response | Promise<Response>
184
- app.use(mw) // global
185
- app.use('/admin', mw) // path-scoped
186
- app.get('/admin', mw, handler) // route-level
208
+ app.use(mw) // global
209
+ app.use('/admin', mw) // path-scoped
210
+ app.get('/admin', mw, handler) // route-level
187
211
  ```
188
212
 
189
213
  ### Context
190
214
 
191
215
  The `ctx` object accumulates properties as it passes through the middleware chain. Below are all documented properties:
192
216
 
193
- | Property | Set by | Type | Description |
194
- |----------|--------|------|-------------|
195
- | `params` | Router | `Record<string, string>` | URL path parameters |
196
- | `query` | Router | `Record<string, string>` | URL query parameters |
197
- | `mountPath` | Router | `string` | Current sub-router mount prefix |
198
- | `env` | `loadEnv()` | `Record<string, string>` | Public env vars (`WEIFUWU_PUBLIC_*`) |
199
- | `csrf.token` | `csrf()` | `string` | CSRF token (namespace) |
200
- | `requestId` | `requestId()` | `string` | Request ID |
201
- | `session` | `session()` | `Session` | Session data object |
202
- | `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
203
- | `redis` | `redis()` | `Redis` | Redis client |
204
- | `ai` | `aiProvider()` | `AIProvider` | AI model & embedding |
205
- | `queue` | `queue()` | `Queue` | Job queue |
206
- | `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
207
- | `permissions` | `permissions()` | `{ roles, permissions }` | RBAC roles & permissions sets |
208
- | `theme` | `theme()` | `{ value, set }` | Current theme + switcher |
209
- | `i18n` | `i18n()` | `{ locale, t, set }` | Locale, translation, switcher |
210
- | `flash` | `flash()` | `{ value, set }` | Flash message + setter |
211
- | `tailwind` | `tailwindContext()` | `{ css, url }` | Compiled Tailwind CSS |
212
- | `tenant` | `tenant()` | `TenantContext` | Current tenant info |
213
- | `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
214
- | `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
215
- | `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
216
- | `mountPath` | `Router` | `string` | Sub-router mount path |
217
- | `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
217
+ | Property | Set by | Type | Description |
218
+ | ------------- | -------------------------------- | ------------------------- | ------------------------------------ |
219
+ | `params` | Router | `Record<string, string>` | URL path parameters |
220
+ | `query` | Router | `Record<string, string>` | URL query parameters |
221
+ | `mountPath` | Router | `string` | Current sub-router mount prefix |
222
+ | `env` | `loadEnv()` | `Record<string, string>` | Public env vars (`WEIFUWU_PUBLIC_*`) |
223
+ | `csrf.token` | `csrf()` | `string` | CSRF token (namespace) |
224
+ | `requestId` | `requestId()` | `string` | Request ID |
225
+ | `session` | `session()` | `Session` | Session data object |
226
+ | `sql` | `postgres()` | `Sql<{}>` | PostgreSQL tagged-template client |
227
+ | `redis` | `redis()` | `Redis` | Redis client |
228
+ | `ai` | `aiProvider()` | `AIProvider` | AI model & embedding |
229
+ | `queue` | `queue()` | `Queue` | Job queue |
230
+ | `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
231
+ | `permissions` | `permissions()` | `{ roles, permissions }` | RBAC roles & permissions sets |
232
+ | `theme` | `theme()` | `{ value, set }` | Current theme + switcher |
233
+ | `i18n` | `i18n()` | `{ locale, t, set }` | Locale, translation, switcher |
234
+ | `flash` | `flash()` | `{ value, set }` | Flash message + setter |
235
+ | `tailwind` | `tailwindContext()` | `{ css, url }` | Compiled Tailwind CSS |
236
+ | `tenant` | `tenant()` | `TenantContext` | Current tenant info |
237
+ | `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
238
+ | `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
239
+ | `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
240
+ | `mountPath` | `Router` | `string` | Sub-router mount path |
241
+ | `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
218
242
 
219
243
  ### Type-Safe Context
220
244
 
@@ -222,13 +246,13 @@ Middleware-injected properties are **automatically typed** through chained `use(
222
246
 
223
247
  ```ts
224
248
  const app = new Router()
225
- .use(csrf()) // → Router<Context & { csrf: { token: string } }>
226
- .use(requestId()) // → Router<Context & { csrf: ..., requestId }>
227
- .use(postgres()) // → Router<Context & { csrf: ..., requestId, sql }>
249
+ .use(csrf()) // → Router<Context & { csrf: { token: string } }>
250
+ .use(requestId()) // → Router<Context & { csrf: ..., requestId }>
251
+ .use(postgres()) // → Router<Context & { csrf: ..., requestId, sql }>
228
252
 
229
253
  app.get('/me', (_req, ctx) => {
230
- ctx.csrf.token // ✅ string (IDE autocomplete)
231
- ctx.requestId // ✅ string
254
+ ctx.csrf.token // ✅ string (IDE autocomplete)
255
+ ctx.requestId // ✅ string
232
256
  ctx.sql`SELECT 1` // ✅ Sql<{}>
233
257
  })
234
258
  ```
@@ -241,43 +265,44 @@ Each module exports an `XxxInjected` type (e.g. `PostgresInjected`, `UserInjecte
241
265
 
242
266
  All modules follow one of **4 patterns** — learn these and you know every module.
243
267
 
244
- | Pattern | How to mount | Example |
245
- |---------|-------------|---------|
246
- | `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
247
- | `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
248
- | `[γ]` | Import and call directly | `mailer()`, `fts`, `cron-utils` |
249
- | `[δ]` | `import { useXxx } from 'weifuwu/react'` | `useTheme()`, `useLocale()`, `useWebsocket()` |
268
+ | Pattern | How to mount | Example |
269
+ | ------- | ---------------------------------------- | ------------------------------------------------------ |
270
+ | `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
271
+ | `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
272
+ | `[γ]` | Import and call directly | `mailer()`, `fts`, `cron-utils` |
273
+ | `[δ]` | `import { useXxx } from 'weifuwu/react'` | `useTheme()`, `useLocale()`, `useWebsocket()` |
250
274
 
251
275
  ### Pattern α — Middleware
252
276
 
253
277
  ```ts
254
- app.use(compress()) // basic
255
- const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
278
+ app.use(compress()) // basic
279
+ const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
256
280
  app.use(pg)
257
- app.use(rateLimit({ max: 100 })) // with .close()
281
+ app.use(rateLimit({ max: 100 })) // with .close()
258
282
  ```
259
283
 
260
284
  ### Pattern β — Router
261
285
 
262
286
  ```ts
263
- app.use('/health', health()) // with path
287
+ app.use('/health', health()) // with path
264
288
  app.use('/graphql', graphql(handler))
265
- app.use('/logs', logdb({ pg })) // with .log(), .migrate()
266
- app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
289
+ app.use('/logs', logdb({ pg })) // with .log(), .migrate()
290
+ app.use('/auth', user({ pg, jwtSecret })) // with .middleware(), .register()
267
291
  app.ws('/ws', messager({ pg }).wsHandler())
268
292
  ```
269
293
 
270
294
  β modules that need **separate middleware** use `.middleware()`. Most can auto-register both middleware and routes in one call:
295
+
271
296
  ```ts
272
- app.use(theme()) // auto: middleware + /__theme/:value
273
- app.use(i18n({ dir: './locales' })) // auto: middleware + /__lang/:locale
297
+ app.use(theme()) // auto: middleware + /__theme/:value
298
+ app.use(i18n({ dir: './locales' })) // auto: middleware + /__lang/:locale
274
299
  app.use(analytics({ pg })) // auto: middleware + /__analytics
275
- app.use(auth) // auto: middleware + /register, /login (user())
300
+ app.use(auth) // auto: middleware + /register, /login (user())
276
301
 
277
302
  // Explicit form when more control is needed:
278
303
  const a = analytics()
279
- app.use(a.middleware()) // tracking only
280
- app.use('/', a) // dashboard at custom path
304
+ app.use(a.middleware()) // tracking only
305
+ app.use('/', a) // dashboard at custom path
281
306
  ```
282
307
 
283
308
  ### Pattern γ — Standalone
@@ -290,7 +315,7 @@ import { mailer, cronNext, fts } from 'weifuwu'
290
315
  const email = mailer({ transport: 'smtp://...', from: 'noreply@example.com' })
291
316
  await email.send({ to: 'user@test.com', subject: 'Hello', text: 'Body' })
292
317
 
293
- const next = cronNext('0 9 * * 1-5') // next weekday at 09:00
318
+ const next = cronNext('0 9 * * 1-5') // next weekday at 09:00
294
319
  ```
295
320
 
296
321
  ### Pattern δ — Client-side
@@ -346,54 +371,54 @@ graph TD
346
371
 
347
372
  ## Quick Module Selection
348
373
 
349
- | What do you want to do? | Module | Pattern |
350
- |------------------------|--------|---------|
351
- | **User registration / login** | `user()` | β |
352
- | **Simple token/header auth** | `auth()` | α |
353
- | **JWT verification** | `user().middleware()` | α |
354
- | **Role-based access control** | `permissions()` | α |
355
- | **AI chat / generate / stream** | `ctx.ai.generateText()` / `ctx.ai.streamText()` | α (via `aiProvider()`) |
356
- | **AI agent with knowledge** | `agent()` + `knowledgeBase()` | β |
357
- | **Send email** | `mailer()` | γ |
358
- | **File upload** | `upload()` | α |
359
- | **Object storage (S3/MinIO)** | `s3()` | α |
360
- | **Rate limiting** | `rateLimit()` | α |
361
- | **Response caching** | `cache()` | α |
362
- | **Periodic / delayed jobs** | `queue()` | α |
363
- | **Page view analytics** | `analytics()` | β |
364
- | **Structured logging** | `logdb()` | β |
365
- | **Real-time chat / messager** | `messager()` | β |
366
- | **Full-text search** | `fts` | γ |
367
- | **Theme switching** | `theme()` | α |
368
- | **i18n / localization** | `i18n()` | α |
369
- | **Flash messages** | `flash()` | α |
370
- | **Server-Sent Events** | `createSSEStream()` | γ |
371
- | **GraphQL endpoint** | `graphql()` | β |
372
- | **Webhook receiver** | `webhook()` | β |
373
- | **SSR with React** | `ssr()` | β |
374
- | **Health check** | `health()` | β |
375
- | **SEO (robots.txt, sitemap)** | `seo()` | β |
376
- | **Multi-process deploy** | `deploy()` | γ |
377
- | **Distributed functions (iii)** | `iii()` | β |
378
- | **Multi-tenant BaaS** | `tenant()` | β |
379
- | **Client-side routing** | `useNavigate()`, `<Link>` | δ |
380
- | **WebSocket in React** | `useWebsocket()` | δ |
381
- | **Compression (brotli/gzip)** | `compress()` | α |
382
- | **Security headers (CSP, HSTS)** | `helmet()` | α |
383
- | **CORS** | `cors()` | α |
384
- | **CSRF protection** | `csrf()` | α |
385
- | **Request ID tracing** | `requestId()` | α |
386
- | **Environment variables** | `env()` / `loadEnv()` | α |
387
- | **Static file serving** | `serveStatic()` | α |
388
- | **Object storage (S3/MinIO)** | `s3()` | α |
389
- | **Send email** | `mailer()` | γ |
390
- | **Scheduled / cron tasks** | `cron-utils` (`cronNext()`) | γ |
391
- | **Server-Sent Events** | `createSSEStream()` | γ |
392
- | **Multi-process deploy** | `deploy()` | γ |
393
- | **Distributed functions (iii)** | `iii()` | β |
394
- | **Webhook receiver** | `webhook()` | β |
395
- | **Social login (OAuth)** | `user({ oauthLogin })` | β |
396
- | **Database migrations** | `pg.migrate()` | — |
374
+ | What do you want to do? | Module | Pattern |
375
+ | -------------------------------- | ----------------------------------------------- | ---------------------- |
376
+ | **User registration / login** | `user()` | β |
377
+ | **Simple token/header auth** | `auth()` | α |
378
+ | **JWT verification** | `user().middleware()` | α |
379
+ | **Role-based access control** | `permissions()` | α |
380
+ | **AI chat / generate / stream** | `ctx.ai.generateText()` / `ctx.ai.streamText()` | α (via `aiProvider()`) |
381
+ | **AI agent with knowledge** | `agent()` + `knowledgeBase()` | β |
382
+ | **Send email** | `mailer()` | γ |
383
+ | **File upload** | `upload()` | α |
384
+ | **Object storage (S3/MinIO)** | `s3()` | α |
385
+ | **Rate limiting** | `rateLimit()` | α |
386
+ | **Response caching** | `cache()` | α |
387
+ | **Periodic / delayed jobs** | `queue()` | α |
388
+ | **Page view analytics** | `analytics()` | β |
389
+ | **Structured logging** | `logdb()` | β |
390
+ | **Real-time chat / messager** | `messager()` | β |
391
+ | **Full-text search** | `fts` | γ |
392
+ | **Theme switching** | `theme()` | α |
393
+ | **i18n / localization** | `i18n()` | α |
394
+ | **Flash messages** | `flash()` | α |
395
+ | **Server-Sent Events** | `createSSEStream()` | γ |
396
+ | **GraphQL endpoint** | `graphql()` | β |
397
+ | **Webhook receiver** | `webhook()` | β |
398
+ | **SSR with React** | `ssr()` | β |
399
+ | **Health check** | `health()` | β |
400
+ | **SEO (robots.txt, sitemap)** | `seo()` | β |
401
+ | **Multi-process deploy** | `deploy()` | γ |
402
+ | **Distributed functions (iii)** | `iii()` | β |
403
+ | **Multi-tenant BaaS** | `tenant()` | β |
404
+ | **Client-side routing** | `useNavigate()`, `<Link>` | δ |
405
+ | **WebSocket in React** | `useWebsocket()` | δ |
406
+ | **Compression (brotli/gzip)** | `compress()` | α |
407
+ | **Security headers (CSP, HSTS)** | `helmet()` | α |
408
+ | **CORS** | `cors()` | α |
409
+ | **CSRF protection** | `csrf()` | α |
410
+ | **Request ID tracing** | `requestId()` | α |
411
+ | **Environment variables** | `env()` / `loadEnv()` | α |
412
+ | **Static file serving** | `serveStatic()` | α |
413
+ | **Object storage (S3/MinIO)** | `s3()` | α |
414
+ | **Send email** | `mailer()` | γ |
415
+ | **Scheduled / cron tasks** | `cron-utils` (`cronNext()`) | γ |
416
+ | **Server-Sent Events** | `createSSEStream()` | γ |
417
+ | **Multi-process deploy** | `deploy()` | γ |
418
+ | **Distributed functions (iii)** | `iii()` | β |
419
+ | **Webhook receiver** | `webhook()` | β |
420
+ | **Social login (OAuth)** | `user({ oauthLogin })` | β |
421
+ | **Database migrations** | `pg.migrate()` | — |
397
422
 
398
423
  ---
399
424
 
@@ -412,7 +437,16 @@ app.get('/api', (req, ctx) => {
412
437
  **Structured logging** — `logger({ format: 'json' })` outputs JSON to stderr with `traceId`, `timestamp`, `elapsed_ms`:
413
438
 
414
439
  ```json
415
- {"level":"info","message":"request","method":"GET","path":"/api/users","status":200,"elapsed_ms":42,"traceId":"f240a3f3-...","timestamp":"2025-01-15T10:30:00.000Z"}
440
+ {
441
+ "level": "info",
442
+ "message": "request",
443
+ "method": "GET",
444
+ "path": "/api/users",
445
+ "status": 200,
446
+ "elapsed_ms": 42,
447
+ "traceId": "f240a3f3-...",
448
+ "timestamp": "2025-01-15T10:30:00.000Z"
449
+ }
416
450
  ```
417
451
 
418
452
  Default format is `'short'` (human-readable). `'combined'` includes query strings.
@@ -480,12 +514,12 @@ assert.equal(res.status, 200)
480
514
  assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
481
515
  ```
482
516
 
483
- | Method | Description |
484
- |--------|-------------|
485
- | `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request |
486
- | `.withUser(u)` `.withTenant(t)` `.with(ctx)` | Simulate middleware injection |
487
- | `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
488
- | `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
517
+ | Method | Description |
518
+ | ------------------------------------------------------------ | ----------------------------------------------------- |
519
+ | `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request |
520
+ | `.withUser(u)` `.withTenant(t)` `.with(ctx)` | Simulate middleware injection |
521
+ | `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
522
+ | `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
489
523
 
490
524
  ### Database test isolation
491
525
 
@@ -496,7 +530,7 @@ import { createTestDb, withTestDb } from 'weifuwu'
496
530
  const db = await createTestDb()
497
531
  await db.sql`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
498
532
  await db.sql`INSERT INTO users (name) VALUES ('Alice')`
499
- await db.destroy() // DROP SCHEMA ... CASCADE
533
+ await db.destroy() // DROP SCHEMA ... CASCADE
500
534
 
501
535
  // Transaction rollback — all changes are rolled back after callback
502
536
  await withTestDb(async (sql) => {
@@ -505,10 +539,10 @@ await withTestDb(async (sql) => {
505
539
  })
506
540
  ```
507
541
 
508
- | Function | Description |
509
- |----------|-------------|
510
- | `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
511
- | `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback |
542
+ | Function | Description |
543
+ | ---------------------- | --------------------------------------------------------------- |
544
+ | `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
545
+ | `withTestDb(url?, fn)` | Run callback in a transaction, auto-rollback |
512
546
 
513
547
  Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset.
514
548
 
@@ -533,21 +567,21 @@ await a.addKnowledge(agentId, 'Title', 'some knowledge content')
533
567
  a.run(agentId, { input: 'summarize the data', stream: true })
534
568
  ```
535
569
 
536
- | Option | Type | Default | Description |
537
- |--------|------|---------|-------------|
538
- | `pg` | `object` | — | PostgreSQL client |
539
- | `provider` | `AIProvider` | `aiProvider()` (from env) | AI provider for model & embedding resolution |
540
- | `model` | `object` | — | Explicit AI model (overrides provider) |
541
- | `embeddingModel` | `object` | — | Explicit embedding model (overrides provider) |
542
- | `embeddingDimension` | `number` | `provider.dimension` | Embedding vector dimension |
543
- | `tools` | `object[]` | — | Custom tool definitions |
570
+ | Option | Type | Default | Description |
571
+ | -------------------- | ------------ | ------------------------- | --------------------------------------------- |
572
+ | `pg` | `object` | — | PostgreSQL client |
573
+ | `provider` | `AIProvider` | `aiProvider()` (from env) | AI provider for model & embedding resolution |
574
+ | `model` | `object` | — | Explicit AI model (overrides provider) |
575
+ | `embeddingModel` | `object` | — | Explicit embedding model (overrides provider) |
576
+ | `embeddingDimension` | `number` | `provider.dimension` | Embedding vector dimension |
577
+ | `tools` | `object[]` | — | Custom tool definitions |
544
578
 
545
- | Method | Description |
546
- |--------|-------------|
579
+ | Method | Description |
580
+ | ---------------------------------------------- | ------------------------ |
547
581
  | `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
548
- | `.addKnowledge(agentId, title, content)` | Add knowledge document |
549
- | `.migrate()` | DB setup |
550
- | `.close()` | Cleanup |
582
+ | `.addKnowledge(agentId, title, content)` | Add knowledge document |
583
+ | `.migrate()` | DB setup |
584
+ | `.close()` | Cleanup |
551
585
 
552
586
  ### aiStream [β] [AI]
553
587
 
@@ -559,10 +593,10 @@ const chat = await aiStream(async (req) => ({ messages: (await req.json()).messa
559
593
  app.use('/chat', chat)
560
594
  ```
561
595
 
562
- | Param | Type | Description |
563
- |-------|------|-------------|
564
- | `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
565
- | `provider` | `AIProvider` | Optional. If provided and handler omits `model`, `provider.model()` is used as default |
596
+ | Param | Type | Description |
597
+ | ---------- | ----------------------------------------------------------- | -------------------------------------------------------------------------------------- |
598
+ | `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
599
+ | `provider` | `AIProvider` | Optional. If provided and handler omits `model`, `provider.model()` is used as default |
566
600
 
567
601
  ### analytics [β] [API]
568
602
 
@@ -571,49 +605,52 @@ In-memory or PostgreSQL page view tracking with built-in dashboard.
571
605
  ```ts
572
606
  const a = analytics()
573
607
  app.use(a.middleware())
574
- app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
608
+ app.use('/', a) // GET /__analytics (dashboard), GET /__analytics/data?days=7 (JSON)
575
609
  ```
576
610
 
577
- | Option | Type | Default | Description |
578
- |--------|------|---------|-------------|
579
- | `pg` | `object` | — | PostgreSQL client for persistence |
580
- | `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip |
611
+ | Option | Type | Default | Description |
612
+ | ---------- | ---------- | --------------------------------------- | --------------------------------- |
613
+ | `pg` | `object` | — | PostgreSQL client for persistence |
614
+ | `excluded` | `string[]` | `['/__analytics', '/__wfw', '/static']` | Paths to skip |
581
615
 
582
616
  ```ts
583
617
  // With PostgreSQL
584
618
  const a = analytics({ pg })
585
619
  await a.migrate()
586
620
  app.use(a.middleware())
587
- app.use('/', a) // dashboard routes
621
+ app.use('/', a) // dashboard routes
588
622
  ```
589
623
 
590
624
  ### auth [α] [Security]
591
625
 
592
626
  ```ts
593
- app.use(auth({ token: 'sk-123' })) // static token
594
- app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
627
+ app.use(auth({ token: 'sk-123' })) // static token
628
+ app.use(auth({ header: 'X-API-Key', token: 'my-key' })) // custom header
595
629
  app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
596
630
  app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
597
631
 
598
632
  // Session-based auth (must be placed after session() middleware)
599
633
  app.use(session())
600
- app.use(auth({
601
- session: true,
602
- resolveUser: async (userId) => { // load user from DB
603
- const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
604
- return user ?? null // null destroy stale session
605
- },
606
- }))
634
+ app.use(
635
+ auth({
636
+ session: true,
637
+ resolveUser: async (userId) => {
638
+ // load user from DB
639
+ const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
640
+ return user ?? null // null → destroy stale session
641
+ },
642
+ }),
643
+ )
607
644
  ```
608
645
 
609
- | Option | Type | Default | Description |
610
- |--------|------|---------|-------------|
611
- | `token` | `string` | — | Static token to match |
612
- | `header` | `string` | `'Authorization'` | Header name |
613
- | `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
614
- | `proxy` | `string` | — | Auth service URL to proxy requests to |
615
- | `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
616
- | `resolveUser` | `(userId) => object\|null` | — | Load user from userId (called when `session: true`). Return falsy to reject + auto-destroy stale session |
646
+ | Option | Type | Default | Description |
647
+ | ------------- | ------------------------------ | ----------------- | -------------------------------------------------------------------------------------------------------- |
648
+ | `token` | `string` | — | Static token to match |
649
+ | `header` | `string` | `'Authorization'` | Header name |
650
+ | `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
651
+ | `proxy` | `string` | — | Auth service URL to proxy requests to |
652
+ | `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
653
+ | `resolveUser` | `(userId) => object\|null` | — | Load user from userId (called when `session: true`). Return falsy to reject + auto-destroy stale session |
617
654
 
618
655
  When `session: true`, auth checks `ctx.session.userId` before the
619
656
  Authorization header. This lets logged-in users authenticate via their
@@ -623,32 +660,32 @@ if no session userId is present.
623
660
  ### compress [α] [DevTools]
624
661
 
625
662
  ```ts
626
- app.use(compress()) // brotli > gzip > deflate (min 1KB)
627
- app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and level
663
+ app.use(compress()) // brotli > gzip > deflate (min 1KB)
664
+ app.use(compress({ threshold: 2048, level: 4 })) // custom threshold and level
628
665
  ```
629
666
 
630
- | Option | Type | Default | Description |
631
- |--------|------|---------|-------------|
632
- | `threshold` | `number` | `1024` | Minimum byte size to compress |
633
- | `level` | `number` | `6` | Compression level (zlib) |
667
+ | Option | Type | Default | Description |
668
+ | ----------- | -------- | ------- | ----------------------------- |
669
+ | `threshold` | `number` | `1024` | Minimum byte size to compress |
670
+ | `level` | `number` | `6` | Compression level (zlib) |
634
671
 
635
672
  ### cors [α] [DevTools]
636
673
 
637
674
  ```ts
638
- app.use(cors()) // allow all
639
- app.use(cors({ origin: ['https://example.com'] })) // whitelist
675
+ app.use(cors()) // allow all
676
+ app.use(cors({ origin: ['https://example.com'] })) // whitelist
640
677
  app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
641
678
  app.use(cors({ credentials: true, maxAge: 3600 }))
642
679
  ```
643
680
 
644
- | Option | Type | Default | Description |
645
- |--------|------|---------|-------------|
646
- | `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
647
- | `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
648
- | `allowedHeaders` | `string[]` | — | Custom allowed headers |
649
- | `exposedHeaders` | `string[]` | — | Response headers exposed to client |
650
- | `credentials` | `boolean` | `false` | Allow cookies/credentials |
651
- | `maxAge` | `number` | — | Preflight cache duration (seconds) |
681
+ | Option | Type | Default | Description |
682
+ | ---------------- | ---------------------------- | -------------------------------------------------------- | ---------------------------------- |
683
+ | `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
684
+ | `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
685
+ | `allowedHeaders` | `string[]` | — | Custom allowed headers |
686
+ | `exposedHeaders` | `string[]` | — | Response headers exposed to client |
687
+ | `credentials` | `boolean` | `false` | Allow cookies/credentials |
688
+ | `maxAge` | `number` | — | Preflight cache duration (seconds) |
652
689
 
653
690
  ### flash [α] [UX]
654
691
 
@@ -658,7 +695,7 @@ Cookie-based flash message. Read from request, write via redirect.
658
695
  app.use(flash())
659
696
 
660
697
  app.get('/', (req, ctx) => {
661
- const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
698
+ const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
662
699
  })
663
700
 
664
701
  app.post('/save', (req, ctx) => {
@@ -666,8 +703,8 @@ app.post('/save', (req, ctx) => {
666
703
  })
667
704
  ```
668
705
 
669
- | Option | Type | Default | Description |
670
- |--------|------|---------|-------------|
706
+ | Option | Type | Default | Description |
707
+ | ------ | -------- | --------- | ----------- |
671
708
  | `name` | `string` | `'flash'` | Cookie name |
672
709
 
673
710
  ### cache [α] [DevTools]
@@ -675,30 +712,32 @@ app.post('/save', (req, ctx) => {
675
712
  Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
676
713
 
677
714
  ```ts
678
- app.use(cache()) // in-memory, 5min TTL
679
- app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
680
- app.use(cache({
681
- ttl: 30_000,
682
- tag: (req, ctx) => ctx.user ? `user:${ctx.user.id}` : undefined, // per-user invalidation
683
- }))
715
+ app.use(cache()) // in-memory, 5min TTL
716
+ app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
717
+ app.use(
718
+ cache({
719
+ ttl: 30_000,
720
+ tag: (req, ctx) => (ctx.user ? `user:${ctx.user.id}` : undefined), // per-user invalidation
721
+ }),
722
+ )
684
723
 
685
724
  // Programmatic invalidation
686
725
  const c = cache({ store: 'redis', redis: ctx.redis })
687
726
  app.use(c)
688
- await c.invalidate('users') // invalidate all entries tagged with 'users'
689
- await c.flush() // clear entire cache
727
+ await c.invalidate('users') // invalidate all entries tagged with 'users'
728
+ await c.flush() // clear entire cache
690
729
  ```
691
730
 
692
- | Option | Type | Default | Description |
693
- |--------|------|---------|-------------|
694
- | `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
695
- | `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
696
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
697
- | `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
698
- | `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
699
- | `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
700
- | `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
701
- | `maxBodySize` | `number` | `1048576` (1MB) | Max body bytes to cache |
731
+ | Option | Type | Default | Description |
732
+ | -------------- | ----------------------------------- | ------------------ | --------------------------------------------- |
733
+ | `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
734
+ | `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
735
+ | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
736
+ | `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
737
+ | `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
738
+ | `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
739
+ | `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
740
+ | `maxBodySize` | `number` | `1048576` (1MB) | Max body bytes to cache |
702
741
 
703
742
  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.
704
743
 
@@ -706,7 +745,11 @@ Cached responses include `X-Cache: HIT` and `Age` headers. Requests with `Author
706
745
  import { MemoryCache, RedisCache } from 'weifuwu'
707
746
 
708
747
  const mem = new MemoryCache()
709
- await mem.set('key', { status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] }, 300_000)
748
+ await mem.set(
749
+ 'key',
750
+ { status: 200, statusText: 'OK', headers: {}, body: '...', createdAt: Date.now(), tags: [] },
751
+ 300_000,
752
+ )
710
753
  mem.close()
711
754
  ```
712
755
 
@@ -719,12 +762,12 @@ app.use(csrf())
719
762
  // Falls back to body field matching the key name
720
763
  ```
721
764
 
722
- | Option | Default | Description |
723
- |--------|---------|-------------|
724
- | `cookie` | `'_csrf'` | Cookie name |
725
- | `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
726
- | `key` | `'_csrf'` | Body field fallback |
727
- | `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
765
+ | Option | Default | Description |
766
+ | ---------------- | -------------------------- | ----------------------------------------- |
767
+ | `cookie` | `'_csrf'` | Cookie name |
768
+ | `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
769
+ | `key` | `'_csrf'` | Body field fallback |
770
+ | `excludeMethods` | `['GET','HEAD','OPTIONS']` | Skip validation |
728
771
 
729
772
  ### deploy [β] [Networking]
730
773
 
@@ -734,26 +777,30 @@ Multi-process manager with reverse proxy, health checks, auto-restart, and zero-
734
777
  import { deploy, defineConfig } from 'weifuwu'
735
778
 
736
779
  // Local
737
- await deploy(defineConfig({
738
- apps: { blog: {}, api: {} },
739
- }))
780
+ await deploy(
781
+ defineConfig({
782
+ apps: { blog: {}, api: {} },
783
+ }),
784
+ )
740
785
 
741
786
  // Production
742
- await deploy(defineConfig({
743
- domain: 'example.com',
744
- deployToken: process.env.DEPLOY_TOKEN,
745
- apps: { blog: {}, api: {} },
746
- }))
787
+ await deploy(
788
+ defineConfig({
789
+ domain: 'example.com',
790
+ deployToken: process.env.DEPLOY_TOKEN,
791
+ apps: { blog: {}, api: {} },
792
+ }),
793
+ )
747
794
  ```
748
795
 
749
796
  **Auto-derived defaults** — each app key derives `dir`, `port`, `entry`, and `path`:
750
797
 
751
- | Field | Default | Rule |
752
- |-------|---------|------|
753
- | `dir` | App key | `blog` → `'./blog'` |
754
- | `entry` | `'index.ts'` | Default entry file |
755
- | `port` | `3001+` | Auto-incremented from 3001 |
756
- | `path` | `'/key'` | Only for localhost domain |
798
+ | Field | Default | Rule |
799
+ | ------- | ------------ | -------------------------- |
800
+ | `dir` | App key | `blog` → `'./blog'` |
801
+ | `entry` | `'index.ts'` | Default entry file |
802
+ | `port` | `3001+` | Auto-incremented from 3001 |
803
+ | `path` | `'/key'` | Only for localhost domain |
757
804
 
758
805
  Override any field explicitly:
759
806
 
@@ -777,7 +824,11 @@ apps: {
777
824
  **Blue-green** — zero-downtime via `ports`:
778
825
 
779
826
  ```ts
780
- apps: { blog: { ports: [3001, 3002] } }
827
+ apps: {
828
+ blog: {
829
+ ports: [3001, 3002]
830
+ }
831
+ }
781
832
  ```
782
833
 
783
834
  **WebSocket** — automatically bridged through the gateway.
@@ -786,15 +837,15 @@ apps: { blog: { ports: [3001, 3002] } }
786
837
 
787
838
  **Management API** — all endpoints require `Authorization: Bearer <deployToken>`:
788
839
 
789
- | Endpoint | Method | Description |
790
- |----------|--------|-------------|
791
- | `/_deploy/apps` | GET | List apps |
792
- | `/_deploy/apps/:name` | GET | App details |
793
- | `/_deploy/apps/:name/deploy` | POST | Restart |
794
- | `/_deploy/apps/:name/restart` | POST | Restart |
795
- | `/_deploy/apps/:name/stop` | POST | Stop |
796
- | `/_deploy/apps/:name/start` | POST | Start |
797
- | `/_deploy/apps/:name/logs` | GET | SSE log stream |
840
+ | Endpoint | Method | Description |
841
+ | ----------------------------- | ------ | -------------- |
842
+ | `/_deploy/apps` | GET | List apps |
843
+ | `/_deploy/apps/:name` | GET | App details |
844
+ | `/_deploy/apps/:name/deploy` | POST | Restart |
845
+ | `/_deploy/apps/:name/restart` | POST | Restart |
846
+ | `/_deploy/apps/:name/stop` | POST | Stop |
847
+ | `/_deploy/apps/:name/start` | POST | Start |
848
+ | `/_deploy/apps/:name/logs` | GET | SSE log stream |
798
849
 
799
850
  ```bash
800
851
  curl -H "Authorization: Bearer my-token" http://localhost:3000/_deploy/apps
@@ -811,26 +862,26 @@ Restart=always
811
862
 
812
863
  **DeployConfig:**
813
864
 
814
- | Option | Default | Description |
815
- |--------|---------|-------------|
816
- | `domain` | `'localhost'` | Root domain |
817
- | `port` | `3000` | Gateway port |
818
- | `deployToken` | — | Bearer token for management API |
819
- | `defaultApp` | — | Fallback route |
820
- | `apps` | — | `Record<string, AppConfig>` |
865
+ | Option | Default | Description |
866
+ | ------------- | ------------- | ------------------------------- |
867
+ | `domain` | `'localhost'` | Root domain |
868
+ | `port` | `3000` | Gateway port |
869
+ | `deployToken` | — | Bearer token for management API |
870
+ | `defaultApp` | — | Fallback route |
871
+ | `apps` | — | `Record<string, AppConfig>` |
821
872
 
822
873
  **AppConfig:**
823
874
 
824
- | Field | Default | Description |
825
- |-------|---------|-------------|
826
- | `dir` | App key | Directory containing the app |
827
- | `port` | Auto (3001+) | Internal port |
828
- | `entry` | `'index.ts'` | Entry file |
829
- | `path` | `'/key'` (local) | URL path prefix |
830
- | `env` | — | Environment variables |
831
- | `healthEndpoint` | `/` | Health check path |
832
- | `buildCommand` | — | Build command |
833
- | `ports` | — | `[port, port+1]` for blue-green |
875
+ | Field | Default | Description |
876
+ | ---------------- | ---------------- | ------------------------------- |
877
+ | `dir` | App key | Directory containing the app |
878
+ | `port` | Auto (3001+) | Internal port |
879
+ | `entry` | `'index.ts'` | Entry file |
880
+ | `path` | `'/key'` (local) | URL path prefix |
881
+ | `env` | — | Environment variables |
882
+ | `healthEndpoint` | `/` | Health check path |
883
+ | `buildCommand` | — | Build command |
884
+ | `ports` | — | `[port, port+1]` for blue-green |
834
885
 
835
886
  ### env [α] [DevTools]
836
887
 
@@ -839,8 +890,8 @@ Safe to expose to the client.
839
890
 
840
891
  ```ts
841
892
  import { env, loadEnv } from 'weifuwu'
842
- loadEnv() // Load .env into process.env
843
- app.use(env()) // → ctx.env
893
+ loadEnv() // Load .env into process.env
894
+ app.use(env()) // → ctx.env
844
895
 
845
896
  app.get('/config', (req, ctx) => {
846
897
  return Response.json({ apiUrl: ctx.env.API_URL })
@@ -852,20 +903,20 @@ Helper utilities:
852
903
  ```ts
853
904
  import { isDev, isProd, isBundled, getPublicEnv } from 'weifuwu'
854
905
 
855
- isDev() // NODE_ENV === 'development'
856
- isProd() // NODE_ENV === 'production'
857
- isBundled() // Running from compiled dist/index.js?
858
- getPublicEnv() // { API_URL: '...' } — no middleware needed
906
+ isDev() // NODE_ENV === 'development'
907
+ isProd() // NODE_ENV === 'production'
908
+ isBundled() // Running from compiled dist/index.js?
909
+ getPublicEnv() // { API_URL: '...' } — no middleware needed
859
910
  ```
860
911
 
861
- | Function | Description |
862
- |----------|-------------|
912
+ | Function | Description |
913
+ | ---------------- | ---------------------------------------------------------------- |
863
914
  | `loadEnv(path?)` | Load `.env` file into `process.env` (does not override existing) |
864
- | `env()` | Middleware — injects `ctx.env` with public vars |
865
- | `getPublicEnv()` | Returns `WEIFUWU_PUBLIC_*` vars with prefix stripped |
866
- | `isDev()` | `true` when `NODE_ENV === 'development'` |
867
- | `isProd()` | `true` when `NODE_ENV === 'production'` |
868
- | `isBundled()` | `true` when running from compiled bundle |
915
+ | `env()` | Middleware — injects `ctx.env` with public vars |
916
+ | `getPublicEnv()` | Returns `WEIFUWU_PUBLIC_*` vars with prefix stripped |
917
+ | `isDev()` | `true` when `NODE_ENV === 'development'` |
918
+ | `isProd()` | `true` when `NODE_ENV === 'production'` |
919
+ | `isBundled()` | `true` when running from compiled bundle |
869
920
 
870
921
  ### graphql [β] [API]
871
922
 
@@ -873,22 +924,22 @@ getPublicEnv() // { API_URL: '...' } — no middleware needed
873
924
  const handler: GraphQLHandler = () => ({
874
925
  schema: `type Query { hello: String }`,
875
926
  resolvers: { Query: { hello: () => 'world' } },
876
- graphiql: true, // GET / returns GraphiQL IDE
877
- maxDepth: 10, // max query nesting (default 10, 0 = disable)
878
- timeout: 30_000, // execution timeout in ms
927
+ graphiql: true, // GET / returns GraphiQL IDE
928
+ maxDepth: 10, // max query nesting (default 10, 0 = disable)
929
+ timeout: 30_000, // execution timeout in ms
879
930
  })
880
931
  app.use('/graphql', graphql(handler))
881
932
  ```
882
933
 
883
- | Option | Type | Default | Description |
884
- |--------|------|---------|-------------|
885
- | `schema` | `string \| GraphQLSchema` | — | SDL string or pre-built schema |
886
- | `resolvers` | `object` | — | Resolver map |
887
- | `rootValue` | `any` | — | Root value for queries |
888
- | `context` | `(req, ctx) => object` | — | Per-request context factory |
889
- | `graphiql` | `boolean` | `false` | Serve GraphiQL IDE at GET / |
890
- | `maxDepth` | `number` | `10` | Max query nesting depth |
891
- | `timeout` | `number` | `30_000` | Execution timeout (ms) |
934
+ | Option | Type | Default | Description |
935
+ | ----------- | ------------------------- | -------- | ------------------------------ |
936
+ | `schema` | `string \| GraphQLSchema` | — | SDL string or pre-built schema |
937
+ | `resolvers` | `object` | — | Resolver map |
938
+ | `rootValue` | `any` | — | Root value for queries |
939
+ | `context` | `(req, ctx) => object` | — | Per-request context factory |
940
+ | `graphiql` | `boolean` | `false` | Serve GraphiQL IDE at GET / |
941
+ | `maxDepth` | `number` | `10` | Max query nesting depth |
942
+ | `timeout` | `number` | `30_000` | Execution timeout (ms) |
892
943
 
893
944
  ### health [β] [API]
894
945
 
@@ -897,10 +948,10 @@ app.use('/health', health())
897
948
  // Returns 200 on success, 503 when check throws
898
949
  ```
899
950
 
900
- | Option | Type | Default | Description |
901
- |--------|------|---------|-------------|
902
- | `path` | `string` | `'/health'` | Health check endpoint |
903
- | `check` | `() => Promise<void>` | — | Async function; throws → 503 |
951
+ | Option | Type | Default | Description |
952
+ | ------- | --------------------- | ----------- | ---------------------------- |
953
+ | `path` | `string` | `'/health'` | Health check endpoint |
954
+ | `check` | `() => Promise<void>` | — | Async function; throws → 503 |
904
955
 
905
956
  ### helmet [α] [Security]
906
957
 
@@ -911,17 +962,17 @@ app.use(helmet())
911
962
  app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))
912
963
  ```
913
964
 
914
- | Option | Default | Description |
915
- |--------|---------|-------------|
916
- | `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
917
- | `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
918
- | `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
919
- | `referrerPolicy` | `'no-referrer'` | Referrer header |
920
- | `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
921
- | `permissionsPolicy` | — | Feature permissions policy |
922
- | `crossOriginEmbedderPolicy` | — | COEP header |
923
- | `crossOriginOpenerPolicy` | — | COOP header |
924
- | `crossOriginResourcePolicy` | — | CORP header |
965
+ | Option | Default | Description |
966
+ | --------------------------- | --------------------------------------- | -------------------------- |
967
+ | `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
968
+ | `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
969
+ | `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
970
+ | `referrerPolicy` | `'no-referrer'` | Referrer header |
971
+ | `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
972
+ | `permissionsPolicy` | — | Feature permissions policy |
973
+ | `crossOriginEmbedderPolicy` | — | COEP header |
974
+ | `crossOriginOpenerPolicy` | — | COOP header |
975
+ | `crossOriginResourcePolicy` | — | CORP header |
925
976
 
926
977
  ### iii [β] — Worker / Function / Trigger [API]
927
978
 
@@ -934,30 +985,30 @@ app.use('/iii', engine)
934
985
  app.ws('/iii', engine.wsHandler())
935
986
 
936
987
  const w = createWorker('orders')
937
- w.registerFunction('orders::create', async (payload) => db.query('INSERT INTO orders ...', [payload.items]))
988
+ w.registerFunction('orders::create', async (payload) =>
989
+ db.query('INSERT INTO orders ...', [payload.items]),
990
+ )
938
991
  engine.addWorker(w)
939
992
  await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
940
993
  ```
941
994
 
942
- | Option | Type | Default | Description |
943
- |--------|------|---------|-------------|
944
- | `pg` | `object` | — | PostgreSQL client for persistent triggers |
945
- | `redis` | `object` | — | Redis client for streams |
946
- | `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
947
-
948
- | Method | Description |
949
- |--------|-------------|
950
- | `.addWorker(w)` | Register a worker |
951
- | `.removeWorker(w)` | Remove a worker |
952
- | `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
953
- | `.listWorkers()` | List registered workers |
954
- | `.listFunctions()` | List registered functions |
955
- | `.listTriggers()` | List registered triggers |
956
- | `.wsHandler()` | WebSocket handler |
957
- | `.migrate()` | DB setup |
958
- | `.shutdown()` | Clean shutdown |
959
-
960
-
995
+ | Option | Type | Default | Description |
996
+ | ----------- | -------- | ------- | --------------------------------------------- |
997
+ | `pg` | `object` | — | PostgreSQL client for persistent triggers |
998
+ | `redis` | `object` | — | Redis client for streams |
999
+ | `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
1000
+
1001
+ | Method | Description |
1002
+ | ---------------------------------------------------------- | ------------------------- |
1003
+ | `.addWorker(w)` | Register a worker |
1004
+ | `.removeWorker(w)` | Remove a worker |
1005
+ | `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
1006
+ | `.listWorkers()` | List registered workers |
1007
+ | `.listFunctions()` | List registered functions |
1008
+ | `.listTriggers()` | List registered triggers |
1009
+ | `.wsHandler()` | WebSocket handler |
1010
+ | `.migrate()` | DB setup |
1011
+ | `.shutdown()` | Clean shutdown |
961
1012
 
962
1013
  ### knowledgeBase [β] — RAG with pgvector [AI]
963
1014
 
@@ -998,21 +1049,20 @@ app.get('/search', async (req, ctx) => {
998
1049
  })
999
1050
  ```
1000
1051
 
1001
- | Option | Type | Default | Description |
1002
- |--------|------|---------|-------------|
1003
- | `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
1004
- | `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
1005
- | `table` | `string` | `'_kb_docs'` | Database table name |
1006
- | `chunkSize` | `number` | `512` | Max characters per chunk |
1007
- | `chunkOverlap` | `number` | `64` | Overlap between chunks |
1008
- | `searchLimit` | `number` | `5` | Default search result count |
1009
- | `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
1052
+ | Option | Type | Default | Description |
1053
+ | ----------------- | ---------------- | ------------ | --------------------------------------- |
1054
+ | `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
1055
+ | `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
1056
+ | `table` | `string` | `'_kb_docs'` | Database table name |
1057
+ | `chunkSize` | `number` | `512` | Max characters per chunk |
1058
+ | `chunkOverlap` | `number` | `64` | Overlap between chunks |
1059
+ | `searchLimit` | `number` | `5` | Default search result count |
1060
+ | `searchThreshold` | `number` | `0` | Minimum similarity (0–1) |
1010
1061
 
1011
1062
  Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
1012
1063
  replaces old chunks. Provider's `embed()` is used automatically.
1013
1064
  The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
1014
1065
 
1015
-
1016
1066
  ### logdb [β] [API]
1017
1067
 
1018
1068
  PostgreSQL structured event logging with monthly partitioning.
@@ -1021,53 +1071,60 @@ PostgreSQL structured event logging with monthly partitioning.
1021
1071
  const logger = logdb({ pg })
1022
1072
  await logger.migrate()
1023
1073
  app.use('/logs', logger)
1024
- await logger.clean(12) // drop partitions older than 12 months
1074
+ await logger.clean(12) // drop partitions older than 12 months
1025
1075
  await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
1026
1076
  ```
1027
1077
 
1028
- | Option | Type | Default | Description |
1029
- |--------|------|---------|-------------|
1030
- | `pg` | `object` | — | PostgreSQL client |
1031
- | `table` | `string` | `'_log_entries'` | Table name |
1078
+ | Option | Type | Default | Description |
1079
+ | ------- | -------- | ---------------- | ----------------- |
1080
+ | `pg` | `object` | — | PostgreSQL client |
1081
+ | `table` | `string` | `'_log_entries'` | Table name |
1032
1082
 
1033
- | Method | Path | Description |
1034
- |--------|------|-------------|
1035
- | POST | `/` | Create log entry |
1036
- | GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
1037
- | GET | `/:id` | Get single entry |
1083
+ | Method | Path | Description |
1084
+ | ------ | ------ | ---------------------------------------------------------------- |
1085
+ | POST | `/` | Create log entry |
1086
+ | GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
1087
+ | GET | `/:id` | Get single entry |
1038
1088
 
1039
1089
  ### logger [α] [DevTools]
1040
1090
 
1041
1091
  ```ts
1042
- app.use(logger()) // GET /hello 200 5ms
1043
- app.use(logger({ format: 'combined' })) // with query params
1092
+ app.use(logger()) // GET /hello 200 5ms
1093
+ app.use(logger({ format: 'combined' })) // with query params
1044
1094
  ```
1045
1095
 
1046
- | Option | Type | Default | Description |
1047
- |--------|------|---------|-------------|
1096
+ | Option | Type | Default | Description |
1097
+ | -------- | --------------------------------- | --------- | ------------------------------------------------------------- |
1048
1098
  | `format` | `'short' \| 'combined' \| 'json'` | `'short'` | Log format: path only, path + query params, or JSON to stderr |
1049
1099
 
1050
1100
  ### mailer [γ] [Networking]
1051
1101
 
1052
1102
  ```ts
1053
- const mail = mailer({ from: 'noreply@example.com', transport: 'smtp://user:pass@smtp.example.com:587' })
1054
- await mail.send({ to: 'user@test.com', subject: 'Hello', text: 'Body', html: '<p>Body</p>', cc: 'admin@test.com' })
1103
+ const mail = mailer({
1104
+ from: 'noreply@example.com',
1105
+ transport: 'smtp://user:pass@smtp.example.com:587',
1106
+ })
1107
+ await mail.send({
1108
+ to: 'user@test.com',
1109
+ subject: 'Hello',
1110
+ text: 'Body',
1111
+ html: '<p>Body</p>',
1112
+ cc: 'admin@test.com',
1113
+ })
1055
1114
  ```
1056
1115
 
1057
- | Option | Type | Default | Description |
1058
- |--------|------|---------|-------------|
1059
- | `transport` | `string\|object` | — | Nodemailer transport config or connection string |
1060
- | `from` | `string` | — | Default sender address |
1061
- | `send` | `function` | — | Custom send function (alternative to transport) |
1062
-
1063
-
1116
+ | Option | Type | Default | Description |
1117
+ | ----------- | ---------------- | ------- | ------------------------------------------------ |
1118
+ | `transport` | `string\|object` | — | Nodemailer transport config or connection string |
1119
+ | `from` | `string` | — | Default sender address |
1120
+ | `send` | `function` | — | Custom send function (alternative to transport) |
1064
1121
 
1065
1122
  ### oauthLogin (via user()) — Social login (OAuth 2.0 client) [Security]
1066
1123
 
1067
1124
  Social login is built into the [`user()`](#user-β) module via the `oauthLogin` option — no separate import needed.
1068
1125
 
1069
1126
  ```ts
1070
- app.use(session()) // required — stores OAuth state
1127
+ app.use(session()) // required — stores OAuth state
1071
1128
  const u = user({
1072
1129
  pg,
1073
1130
  jwtSecret: process.env.JWT_SECRET!,
@@ -1086,7 +1143,7 @@ const u = user({
1086
1143
  },
1087
1144
  })
1088
1145
  await u.migrate()
1089
- app.use(u) // POST /register, POST /login, GET /auth/:provider, GET /auth/:provider/callback
1146
+ app.use(u) // POST /register, POST /login, GET /auth/:provider, GET /auth/:provider/callback
1090
1147
  ```
1091
1148
 
1092
1149
  **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).
@@ -1119,14 +1176,13 @@ const u = user({
1119
1176
  })
1120
1177
  ```
1121
1178
 
1122
- | Option (oauthLogin) | Type | Default | Description |
1123
- |--------|------|---------|-------------|
1124
- | `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
1125
- | `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
1179
+ | Option (oauthLogin) | Type | Default | Description |
1180
+ | ------------------- | ------------------------------------- | ------- | ------------------------------------------------------------------- |
1181
+ | `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
1182
+ | `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
1126
1183
 
1127
1184
  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.
1128
1185
 
1129
-
1130
1186
  ### messager [β] [Networking]
1131
1187
 
1132
1188
  Real-time chat with channels, WebSocket, agent routing.
@@ -1139,20 +1195,18 @@ app.ws('/ws', msg.wsHandler())
1139
1195
  await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
1140
1196
  ```
1141
1197
 
1142
- | Option | Type | Default | Description |
1143
- |--------|------|---------|-------------|
1144
- | `pg` | `object` | — | PostgreSQL client |
1145
- | `agents` | `AgentModule` | — | Agent module for routing |
1146
- | `webhookTimeout` | `number` | — | Webhook timeout |
1147
- | `redis` | `object` | — | Redis client |
1148
-
1149
- | Method | Description |
1150
- |--------|-------------|
1151
- | `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
1152
- | `.send(channel, content, opts?)` | Send message to channel |
1153
- | `.close()` | Cleanup |
1154
-
1198
+ | Option | Type | Default | Description |
1199
+ | ---------------- | ------------- | ------- | ------------------------ |
1200
+ | `pg` | `object` | — | PostgreSQL client |
1201
+ | `agents` | `AgentModule` | — | Agent module for routing |
1202
+ | `webhookTimeout` | `number` | — | Webhook timeout |
1203
+ | `redis` | `object` | — | Redis client |
1155
1204
 
1205
+ | Method | Description |
1206
+ | -------------------------------- | --------------------------------------------------- |
1207
+ | `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
1208
+ | `.send(channel, content, opts?)` | Send message to channel |
1209
+ | `.close()` | Cleanup |
1156
1210
 
1157
1211
  ### opencode [β] [AI]
1158
1212
 
@@ -1170,35 +1224,35 @@ app.use('/opencode', oc)
1170
1224
  app.ws('/opencode', oc.wsHandler())
1171
1225
  ```
1172
1226
 
1173
- | Option | Type | Default | Description |
1174
- |--------|------|---------|-------------|
1175
- | `pg` | `object` | — | PostgreSQL client |
1176
- | `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
1177
- | `baseURL` | `string` | — | OpenAI-compatible API base URL |
1178
- | `apiKey` | `string` | — | API key for the model |
1179
- | `workspace` | `string` | — | Project directory |
1180
- | `systemPrompt` | `string` | — | Custom system prompt |
1181
- | `skills` | `object[]` | — | Custom skill definitions |
1182
- | `permissions` | `object` | — | Tool permission rules |
1227
+ | Option | Type | Default | Description |
1228
+ | -------------- | ---------- | ------- | ------------------------------------------------------ |
1229
+ | `pg` | `object` | — | PostgreSQL client |
1230
+ | `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
1231
+ | `baseURL` | `string` | — | OpenAI-compatible API base URL |
1232
+ | `apiKey` | `string` | — | API key for the model |
1233
+ | `workspace` | `string` | — | Project directory |
1234
+ | `systemPrompt` | `string` | — | Custom system prompt |
1235
+ | `skills` | `object[]` | — | Custom skill definitions |
1236
+ | `permissions` | `object` | — | Tool permission rules |
1183
1237
 
1184
1238
  ### postgres [α] [Database]
1185
1239
 
1186
1240
  Type-safe PostgreSQL client with schema builder, CRUD, migrations, soft delete, and JSONB/vector support.
1187
1241
 
1188
1242
  ```ts
1189
- const pg = postgres() // reads DATABASE_URL
1190
- app.use(pg) // injects ctx.sql
1243
+ const pg = postgres() // reads DATABASE_URL
1244
+ app.use(pg) // injects ctx.sql
1191
1245
  ```
1192
1246
 
1193
- | Option | Type | Default | Description |
1194
- |--------|------|---------|-------------|
1195
- | `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
1196
- | `max` | `number` | `10` | Max pool connections |
1197
- | `ssl` | `boolean\|object` | — | SSL options |
1198
- | `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
1199
- | `connect_timeout` | `number` | `30` | Connection timeout |
1200
- | `statementTimeout` | `number` | `30_000` | Per-statement timeout (ms, 0 = disable) |
1201
- | `onQuery` | `(query, ms, rows) => void` | — | Query logging callback |
1247
+ | Option | Type | Default | Description |
1248
+ | ------------------ | --------------------------- | ------------------ | --------------------------------------- |
1249
+ | `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
1250
+ | `max` | `number` | `10` | Max pool connections |
1251
+ | `ssl` | `boolean\|object` | — | SSL options |
1252
+ | `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
1253
+ | `connect_timeout` | `number` | `30` | Connection timeout |
1254
+ | `statementTimeout` | `number` | `30_000` | Per-statement timeout (ms, 0 = disable) |
1255
+ | `onQuery` | `(query, ms, rows) => void` | — | Query logging callback |
1202
1256
 
1203
1257
  ```ts
1204
1258
  // Raw SQL via tagged template
@@ -1214,40 +1268,46 @@ const users = pg.table('_users', {
1214
1268
  active: boolean('active').default(true),
1215
1269
  ...timestamps(),
1216
1270
  })
1217
- await users.create() // DDL — no need to pass sql
1271
+ await users.create() // DDL — no need to pass sql
1218
1272
  await users.createIndex('email')
1219
1273
 
1220
1274
  // CRUD — sql already bound
1221
1275
  await users.insert({ name: 'Alice' })
1222
- const { count, data } = await users.readMany({ role: 'admin' }, { orderBy: { name: 'asc' }, limit: 10 })
1276
+ const { count, data } = await users.readMany(
1277
+ { role: 'admin' },
1278
+ { orderBy: { name: 'asc' }, limit: 10 },
1279
+ )
1223
1280
  await users.upsert({ email: 'alice@test.com' }, 'email')
1224
1281
 
1225
1282
  // Reuse schema without redefining fields
1226
1283
  import { pgTable } from 'weifuwu'
1227
- const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
1228
- const users = pg.table(usersSchema) // bind — no field duplication
1284
+ const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
1285
+ const users = pg.table(usersSchema) // bind — no field duplication
1229
1286
 
1230
1287
  // Transactions — with auto-retry on deadlock/serialization failure
1231
- await pg.transaction(async (sql) => {
1232
- const txUsers = users.withSql(sql)
1233
- return txUsers.insert({ name: 'Bob' })
1234
- }, { maxRetries: 3 })
1288
+ await pg.transaction(
1289
+ async (sql) => {
1290
+ const txUsers = users.withSql(sql)
1291
+ return txUsers.insert({ name: 'Bob' })
1292
+ },
1293
+ { maxRetries: 3 },
1294
+ )
1235
1295
 
1236
1296
  // Soft delete — automatic if deleted_at column exists
1237
- await users.delete(1) // SET deleted_at = NOW()
1238
- await users.hardDelete(1) // DELETE FROM
1239
- await users.read(1) // auto-filters deleted_at IS NULL (use withDeleted: true to include)
1297
+ await users.delete(1) // SET deleted_at = NOW()
1298
+ await users.hardDelete(1) // DELETE FROM
1299
+ await users.read(1) // auto-filters deleted_at IS NULL (use withDeleted: true to include)
1240
1300
 
1241
1301
  // JSONB queries
1242
1302
  const logs = pg.table('logs', { meta: jsonb<{ service: string }>('meta') })
1243
1303
  await logs.readMany(contains('meta', { service: 'auth' }))
1244
1304
 
1245
1305
  // Connection pool visibility
1246
- console.log(pg.poolStats()) // { active: 3, idle: 7, waiting: 0, max: 10 }
1306
+ console.log(pg.poolStats()) // { active: 3, idle: 7, waiting: 0, max: 10 }
1247
1307
 
1248
1308
  // Migration tracking
1249
- await pg.migrate() // creates _weifuwu_migrations
1250
- await pg.markMigrated('myModule') // idempotent
1309
+ await pg.migrate() // creates _weifuwu_migrations
1310
+ await pg.markMigrated('myModule') // idempotent
1251
1311
  const done = await pg.isMigrated('myModule')
1252
1312
 
1253
1313
  // Partitioned tables
@@ -1261,61 +1321,65 @@ await logs.create({ partitionBy: partitionBy('range', 'created_at') })
1261
1321
  | `pg.table(schema)` | Reusing a schema without duplicating field definitions |
1262
1322
  | `pgTable('t', cols)` | No `pg` reference (utility modules, standalone schema files) |
1263
1323
 
1264
- | Column builder | Type | Notes |
1265
- |---------------|------|-------|
1266
- | `serial(name)` | `number` | Auto-increment |
1267
- | `uuid(name)` | `string` | — |
1268
- | `text(name)` | `string` | — |
1269
- | `integer(name)` | `number` | — |
1270
- | `boolean(name)` / `boolean_(name)` | `boolean` | `_` suffix for JS reserved word |
1271
- | `timestamptz(name)` | `string` | — |
1272
- | `jsonb<T>(name)` | `T` | Generic for typed JSONB access |
1273
- | `textArray(name)` | `string[]` | TEXT[] |
1274
- | `vector(name, dims)` | `number[]` | pgvector support |
1324
+ | Column builder | Type | Notes |
1325
+ | ---------------------------------- | ---------- | ------------------------------- |
1326
+ | `serial(name)` | `number` | Auto-increment |
1327
+ | `uuid(name)` | `string` | — |
1328
+ | `text(name)` | `string` | — |
1329
+ | `integer(name)` | `number` | — |
1330
+ | `boolean(name)` / `boolean_(name)` | `boolean` | `_` suffix for JS reserved word |
1331
+ | `timestamptz(name)` | `string` | — |
1332
+ | `jsonb<T>(name)` | `T` | Generic for typed JSONB access |
1333
+ | `textArray(name)` | `string[]` | TEXT[] |
1334
+ | `vector(name, dims)` | `number[]` | pgvector support |
1275
1335
 
1276
1336
  **Column modifiers:** `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(val)`, `.unique()`, `.references(table, column?, onDelete?)`.
1277
1337
 
1278
1338
  **CRUD methods:**
1279
1339
 
1280
- | Method | Description |
1281
- |--------|-------------|
1282
- | `insert(data)` | INSERT + RETURNING \*, returns the inserted row |
1283
- | `insertMany(data)` | Bulk INSERT + RETURNING \*, returns rows |
1284
- | `read(id, opts?)` | SELECT by detected primary key + auto soft-delete filter |
1285
- | `readMany(where?, opts?)` | Filtered query with `{ count, data }` — auto-filters soft-deleted |
1286
- | `update(id, data)` | UPDATE by primary key + RETURNING \*, returns updated row |
1287
- | `updateMany(where, data)` | Bulk UPDATE, returns affected row count |
1288
- | `delete(id)` | Soft delete if `deleted_at` exists, else hard delete |
1289
- | `hardDelete(id)` | Always DELETE FROM |
1290
- | `deleteMany(where)` | Soft bulk delete if `deleted_at` exists |
1291
- | `hardDeleteMany(where)` | Always DELETE FROM |
1292
- | `upsert(data, conflict)` | INSERT ON CONFLICT DO UPDATE, returns row |
1293
- | `count(where?)` | SELECT COUNT(\*) — auto-filters soft-deleted |
1294
- | `create(opts?)` | CREATE TABLE IF NOT EXISTS |
1295
- | `drop(opts?)` | DROP TABLE IF EXISTS |
1296
- | `createIndex(columns, opts?)` | CREATE INDEX |
1297
- | `createUniqueIndex(columns)` | CREATE UNIQUE INDEX |
1298
- | `withSql(sql)` | Returns copy bound to a different sql (for transactions) |
1340
+ | Method | Description |
1341
+ | ----------------------------- | ----------------------------------------------------------------- |
1342
+ | `insert(data)` | INSERT + RETURNING \*, returns the inserted row |
1343
+ | `insertMany(data)` | Bulk INSERT + RETURNING \*, returns rows |
1344
+ | `read(id, opts?)` | SELECT by detected primary key + auto soft-delete filter |
1345
+ | `readMany(where?, opts?)` | Filtered query with `{ count, data }` — auto-filters soft-deleted |
1346
+ | `update(id, data)` | UPDATE by primary key + RETURNING \*, returns updated row |
1347
+ | `updateMany(where, data)` | Bulk UPDATE, returns affected row count |
1348
+ | `delete(id)` | Soft delete if `deleted_at` exists, else hard delete |
1349
+ | `hardDelete(id)` | Always DELETE FROM |
1350
+ | `deleteMany(where)` | Soft bulk delete if `deleted_at` exists |
1351
+ | `hardDeleteMany(where)` | Always DELETE FROM |
1352
+ | `upsert(data, conflict)` | INSERT ON CONFLICT DO UPDATE, returns row |
1353
+ | `count(where?)` | SELECT COUNT(\*) — auto-filters soft-deleted |
1354
+ | `create(opts?)` | CREATE TABLE IF NOT EXISTS |
1355
+ | `drop(opts?)` | DROP TABLE IF EXISTS |
1356
+ | `createIndex(columns, opts?)` | CREATE INDEX |
1357
+ | `createUniqueIndex(columns)` | CREATE UNIQUE INDEX |
1358
+ | `withSql(sql)` | Returns copy bound to a different sql (for transactions) |
1299
1359
 
1300
1360
  **Where helpers** — composable query conditions:
1301
1361
 
1302
- | Helper | SQL |
1303
- |--------|-----|
1304
- | `eq(col, val)` | `"col" = val` |
1305
- | `ne(col, val)` | `"col" != val` |
1306
- | `gt` / `gte` / `lt` / `lte` | Comparison operators |
1307
- | `isNull(col)` / `isNotNull(col)` | `IS NULL` / `IS NOT NULL` |
1308
- | `like(col, pattern)` | `LIKE` |
1309
- | `contains(col, val)` | `@>` JSONB containment |
1310
- | `in_(col, vals)` | `= ANY(...)` |
1311
- | `and(...)` / `or(...)` / `not(...)` | Boolean composition |
1362
+ | Helper | SQL |
1363
+ | ----------------------------------- | ------------------------- |
1364
+ | `eq(col, val)` | `"col" = val` |
1365
+ | `ne(col, val)` | `"col" != val` |
1366
+ | `gt` / `gte` / `lt` / `lte` | Comparison operators |
1367
+ | `isNull(col)` / `isNotNull(col)` | `IS NULL` / `IS NOT NULL` |
1368
+ | `like(col, pattern)` | `LIKE` |
1369
+ | `contains(col, val)` | `@>` JSONB containment |
1370
+ | `in_(col, vals)` | `= ANY(...)` |
1371
+ | `and(...)` / `or(...)` / `not(...)` | Boolean composition |
1312
1372
 
1313
1373
  **PgModule** — base class for modules that need DB access:
1314
1374
 
1315
1375
  ```ts
1316
1376
  class MyModule extends PgModule {
1317
- async migrate() { /* run DDL */ }
1318
- async getUsers() { return this.table('users', {}).readMany() }
1377
+ async migrate() {
1378
+ /* run DDL */
1379
+ }
1380
+ async getUsers() {
1381
+ return this.table('users', {}).readMany()
1382
+ }
1319
1383
  }
1320
1384
  ```
1321
1385
 
@@ -1333,10 +1397,10 @@ const next = cronNext('0 9 * * 1-5')
1333
1397
  console.log(new Date(next))
1334
1398
  ```
1335
1399
 
1336
- | Function | Description |
1337
- |----------|-------------|
1338
- | `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]` |
1339
- | `matches(fields, date)` | Check if a date matches a parsed pattern |
1400
+ | Function | Description |
1401
+ | ----------------------- | ---------------------------------------------------------- |
1402
+ | `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]` |
1403
+ | `matches(fields, date)` | Check if a date matches a parsed pattern |
1340
1404
  | `cronNext(expr, from?)` | Calculate next matching timestamp (`from` defaults to now) |
1341
1405
 
1342
1406
  ### fts — Full-Text Search (PostgreSQL) [Database]
@@ -1367,11 +1431,11 @@ const results = await fts.search(pg.sql, articles, 'node.js framework', {
1367
1431
  await fts.dropIndex(pg.sql, articles)
1368
1432
  ```
1369
1433
 
1370
- | Function | Description |
1371
- |----------|-------------|
1434
+ | Function | Description |
1435
+ | ---------------------------------------- | ------------------------------ |
1372
1436
  | `createIndex(sql, table, fields, opts?)` | Create GIN/GiST tsvector index |
1373
- | `search(sql, table, query, opts?)` | Search with ts_rank ordering |
1374
- | `dropIndex(sql, table, opts?)` | Drop the index |
1437
+ | `search(sql, table, query, opts?)` | Search with ts_rank ordering |
1438
+ | `dropIndex(sql, table, opts?)` | Drop the index |
1375
1439
 
1376
1440
  Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
1377
1441
 
@@ -1391,10 +1455,10 @@ app.use(theme({ default: 'dark' }))
1391
1455
  // app.use('/', t)
1392
1456
  ```
1393
1457
 
1394
- | Option | Type | Default | Description |
1395
- |--------|------|---------|-------------|
1396
- | `default` | `string` | `'system'` | Default theme |
1397
- | `cookie` | `string` | `'theme'` | Cookie name (empty to disable) |
1458
+ | Option | Type | Default | Description |
1459
+ | --------- | -------- | ---------- | ------------------------------ |
1460
+ | `default` | `string` | `'system'` | Default theme |
1461
+ | `cookie` | `string` | `'theme'` | Cookie name (empty to disable) |
1398
1462
 
1399
1463
  ```ts
1400
1464
  // Server-side switching
@@ -1423,13 +1487,13 @@ app.use(i18n({ default: 'zh', dir: './locales' }))
1423
1487
  // app.use('/', l)
1424
1488
  ```
1425
1489
 
1426
- | Option | Type | Default | Description |
1427
- |--------|------|---------|-------------|
1428
- | `default` | `string` | `'en'` | Default locale |
1429
- | `dir` | `string` | — | Directory with `{locale}.json` files |
1430
- | `messages` | `object` | — | Inline translations: `{ zh: { welcome: '欢迎' } }` |
1431
- | `cookie` | `string` | `'locale'` | Cookie name (empty to disable) |
1432
- | `fromAcceptLanguage` | `boolean` | `true` | Detect from Accept-Language header |
1490
+ | Option | Type | Default | Description |
1491
+ | -------------------- | --------- | ---------- | -------------------------------------------------- |
1492
+ | `default` | `string` | `'en'` | Default locale |
1493
+ | `dir` | `string` | — | Directory with `{locale}.json` files |
1494
+ | `messages` | `object` | — | Inline translations: `{ zh: { welcome: '欢迎' } }` |
1495
+ | `cookie` | `string` | `'locale'` | Cookie name (empty to disable) |
1496
+ | `fromAcceptLanguage` | `boolean` | `true` | Detect from Accept-Language header |
1433
1497
 
1434
1498
  ```ts
1435
1499
  // Handler
@@ -1456,7 +1520,9 @@ const q = queue({ store: 'memory' })
1456
1520
  // const q = queue({ store: 'redis', redis })
1457
1521
 
1458
1522
  // Register cron job (uses the same backend for persistence)
1459
- q.cron('*/5 * * * *', async () => { await cleanCache() })
1523
+ q.cron('*/5 * * * *', async () => {
1524
+ await cleanCache()
1525
+ })
1460
1526
 
1461
1527
  // Or use process/add for full queue semantics
1462
1528
  q.process('send-email', async (job) => {
@@ -1475,55 +1541,55 @@ q.add('weekly-report', {}, { schedule: '0 9 * * 1' })
1475
1541
  q.run()
1476
1542
  ```
1477
1543
 
1478
- | Option | Type | Default | Description |
1479
- |--------|------|---------|-------------|
1480
- | `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
1481
- | `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
1482
- | `url` | `string` | — | Redis URL (alternative to client) |
1483
- | `pg` | `object` | — | PostgreSQL client (required when `store: 'pg'`) |
1484
- | `prefix` | `string` | `'queue'` | Key/table prefix |
1485
- | `pollInterval` | `number` | `200` | Poll interval (ms) |
1486
-
1487
- | Method | Description |
1488
- |--------|-------------|
1489
- | `.cron(pattern, handler)` | Register a cron job (uses process + add internally) |
1490
- | `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`) |
1491
- | `.process(type, handler)` | Register job processor |
1492
- | `.run()` | Start processing |
1493
- | `.stop()` | Stop processing |
1494
- | `.jobs(limit?)` | List pending jobs |
1495
- | `.failedJobs(limit?)` | List failed jobs with error messages |
1496
- | `.retryFailed(jobId)` | Retry a specific failed job |
1497
- | `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
1498
- | `.dashboard()` | Returns a Router with management endpoints |
1499
- | `.close()` | Cleanup |
1544
+ | Option | Type | Default | Description |
1545
+ | -------------- | ----------------------------- | ---------- | ----------------------------------------------- |
1546
+ | `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
1547
+ | `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
1548
+ | `url` | `string` | — | Redis URL (alternative to client) |
1549
+ | `pg` | `object` | — | PostgreSQL client (required when `store: 'pg'`) |
1550
+ | `prefix` | `string` | `'queue'` | Key/table prefix |
1551
+ | `pollInterval` | `number` | `200` | Poll interval (ms) |
1552
+
1553
+ | Method | Description |
1554
+ | ---------------------------- | --------------------------------------------------- |
1555
+ | `.cron(pattern, handler)` | Register a cron job (uses process + add internally) |
1556
+ | `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`) |
1557
+ | `.process(type, handler)` | Register job processor |
1558
+ | `.run()` | Start processing |
1559
+ | `.stop()` | Stop processing |
1560
+ | `.jobs(limit?)` | List pending jobs |
1561
+ | `.failedJobs(limit?)` | List failed jobs with error messages |
1562
+ | `.retryFailed(jobId)` | Retry a specific failed job |
1563
+ | `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
1564
+ | `.dashboard()` | Returns a Router with management endpoints |
1565
+ | `.close()` | Cleanup |
1500
1566
 
1501
1567
  **Schedule (cron) field reference:**
1502
1568
 
1503
- | Field | Range |
1504
- |-------|-------|
1505
- | minute | 0–59 |
1506
- | hour | 0–23 |
1507
- | day of month | 1–31 |
1508
- | month | 1–12 |
1509
- | day of week | 0–6 (0=Sunday) |
1569
+ | Field | Range |
1570
+ | ------------ | -------------- |
1571
+ | minute | 0–59 |
1572
+ | hour | 0–23 |
1573
+ | day of month | 1–31 |
1574
+ | month | 1–12 |
1575
+ | day of week | 0–6 (0=Sunday) |
1510
1576
 
1511
1577
  Supported cron syntax: `*` (any), `*/n` (every n), `n-m` (range), `n,m,o` (list), `n` (exact).
1512
1578
 
1513
1579
  **Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
1514
1580
 
1515
- | Method | Path | Description |
1516
- |--------|------|-------------|
1517
- | GET | `/` | Queue stats + pending/failed counts by type |
1518
- | GET | `/:type/failed` | List failed jobs for a type |
1519
- | POST | `/:type/retry` | Retry all failed jobs of a type |
1520
- | POST | `/retry/:id` | Retry a specific failed job by ID |
1581
+ | Method | Path | Description |
1582
+ | ------ | --------------- | ------------------------------------------- |
1583
+ | GET | `/` | Queue stats + pending/failed counts by type |
1584
+ | GET | `/:type/failed` | List failed jobs for a type |
1585
+ | POST | `/:type/retry` | Retry all failed jobs of a type |
1586
+ | POST | `/retry/:id` | Retry a specific failed job by ID |
1521
1587
 
1522
1588
  ### rateLimit [α] [Security]
1523
1589
 
1524
1590
  ```ts
1525
- app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
1526
- app.get('/api', rateLimit({ max: 10 }), handler) // per-route
1591
+ app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
1592
+ app.get('/api', rateLimit({ max: 10 }), handler) // per-route
1527
1593
  app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
1528
1594
 
1529
1595
  // Multi-process: Redis-backed rate limiting
@@ -1533,31 +1599,31 @@ app.use(rateLimit({ max: 100, store: 'redis', redis: ctx.redis }))
1533
1599
  // m.stop() — clear interval (memory) or Redis cleanup
1534
1600
  ```
1535
1601
 
1536
- | Option | Type | Default | Description |
1537
- |--------|------|---------|-------------|
1538
- | `max` | `number` | `100` | Max requests per window |
1539
- | `window` | `number` | `60_000` | Window duration (ms) |
1540
- | `key` | `(req) => string` | IP-based | Key function |
1541
- | `message` | `string` | `'Too Many Requests'` | 429 response body |
1542
- | `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
1543
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1544
- | `prefix` | `string` | `'ratelimit:'` | Redis key prefix |
1602
+ | Option | Type | Default | Description |
1603
+ | --------- | --------------------- | --------------------- | --------------------------------------------- |
1604
+ | `max` | `number` | `100` | Max requests per window |
1605
+ | `window` | `number` | `60_000` | Window duration (ms) |
1606
+ | `key` | `(req) => string` | IP-based | Key function |
1607
+ | `message` | `string` | `'Too Many Requests'` | 429 response body |
1608
+ | `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
1609
+ | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1610
+ | `prefix` | `string` | `'ratelimit:'` | Redis key prefix |
1545
1611
 
1546
1612
  Redis mode uses `INCR` + `EXPIRE` for atomic counting, enabling accurate rate limiting across multiple server processes. Memory mode is ideal for single-process deployments.
1547
1613
 
1548
1614
  ### redis [α] [Database]
1549
1615
 
1550
1616
  ```ts
1551
- const r = redis() // reads REDIS_URL
1552
- app.use(r) // injects ctx.redis
1617
+ const r = redis() // reads REDIS_URL
1618
+ app.use(r) // injects ctx.redis
1553
1619
  await ctx.redis.set('key', 'value')
1554
1620
  // r.close() — cleanup
1555
1621
  ```
1556
1622
 
1557
- | Option | Type | Default | Description |
1558
- |--------|------|---------|-------------|
1559
- | `url` | `string` | `REDIS_URL` env | Redis connection string |
1560
- | (all ioredis options) | — | — | Passed directly to ioredis |
1623
+ | Option | Type | Default | Description |
1624
+ | --------------------- | -------- | --------------- | -------------------------- |
1625
+ | `url` | `string` | `REDIS_URL` env | Redis connection string |
1626
+ | (all ioredis options) | — | — | Passed directly to ioredis |
1561
1627
 
1562
1628
  ### requestId [α] [DevTools]
1563
1629
 
@@ -1567,10 +1633,10 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
1567
1633
  // Sets X-Request-ID header on responses, available as ctx.requestId
1568
1634
  ```
1569
1635
 
1570
- | Option | Type | Default | Description |
1571
- |--------|------|---------|-------------|
1572
- | `header` | `string` | `'X-Request-ID'` | Header name to read/write |
1573
- | `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
1636
+ | Option | Type | Default | Description |
1637
+ | ----------- | -------------- | --------------------- | ------------------------- |
1638
+ | `header` | `string` | `'X-Request-ID'` | Header name to read/write |
1639
+ | `generator` | `() => string` | `crypto.randomUUID()` | ID generator |
1574
1640
 
1575
1641
  ### trace [α] [DevTools]
1576
1642
 
@@ -1578,20 +1644,20 @@ Request-scoped tracing via `AsyncLocalStorage`. Use as middleware to inject `ctx
1578
1644
 
1579
1645
  ```ts
1580
1646
  import { trace } from 'weifuwu'
1581
- app.use(trace()) // → ctx.trace
1582
- app.use(trace({ header: 'X-Trace-Id' })) // custom header
1647
+ app.use(trace()) // → ctx.trace
1648
+ app.use(trace({ header: 'X-Trace-Id' })) // custom header
1583
1649
 
1584
1650
  app.get('/', (req, ctx) => {
1585
- console.log(ctx.trace.requestId) // 550e8400-e29b-...
1586
- console.log(ctx.trace.traceId) // trace UUID
1587
- console.log(ctx.trace.elapsed()) // ms since request start
1651
+ console.log(ctx.trace.requestId) // 550e8400-e29b-...
1652
+ console.log(ctx.trace.traceId) // trace UUID
1653
+ console.log(ctx.trace.elapsed()) // ms since request start
1588
1654
  })
1589
1655
  ```
1590
1656
 
1591
- | Option | Type | Default | Description |
1592
- |--------|------|---------|-------------|
1593
- | `header` | `string` | `'X-Request-ID'` | Request ID header name |
1594
- | `generator` | `() => string` | `crypto.randomUUID()` | Custom ID generator |
1657
+ | Option | Type | Default | Description |
1658
+ | ----------- | -------------- | --------------------- | ---------------------- |
1659
+ | `header` | `string` | `'X-Request-ID'` | Request ID header name |
1660
+ | `generator` | `() => string` | `crypto.randomUUID()` | Custom ID generator |
1595
1661
 
1596
1662
  Utility functions (also available standalone):
1597
1663
 
@@ -1603,29 +1669,31 @@ const elapsed = traceElapsed() // ms since request started
1603
1669
  runWithTrace(incomingId, () => { ... }) // manual scope
1604
1670
  ```
1605
1671
 
1606
- | Function | Description |
1607
- |----------|-------------|
1608
- | `currentTraceId()` | Current request trace ID, or `undefined` outside a request |
1609
- | `currentTrace()` | Full `{ traceId, startTime }` context |
1610
- | `traceElapsed()` | Milliseconds elapsed since the trace started |
1611
- | `runWithTrace(traceId, fn)` | Execute `fn` inside a trace scope |
1672
+ | Function | Description |
1673
+ | --------------------------- | ---------------------------------------------------------- |
1674
+ | `currentTraceId()` | Current request trace ID, or `undefined` outside a request |
1675
+ | `currentTrace()` | Full `{ traceId, startTime }` context |
1676
+ | `traceElapsed()` | Milliseconds elapsed since the trace started |
1677
+ | `runWithTrace(traceId, fn)` | Execute `fn` inside a trace scope |
1612
1678
 
1613
1679
  ### s3 [α] — S3-compatible object storage [Networking]
1614
1680
 
1615
1681
  ```ts
1616
1682
  import { s3 } from 'weifuwu'
1617
1683
 
1618
- app.use(s3({
1619
- bucket: 'my-app',
1620
- region: 'us-east-1',
1621
- endpoint: process.env.S3_URL, // MinIO / R2 / AWS
1622
- forcePathStyle: true, // required for MinIO
1623
- credentials: {
1624
- accessKeyId: process.env.S3_ACCESS_KEY,
1625
- secretAccessKey: process.env.S3_SECRET_KEY,
1626
- },
1627
- publicUrl: 'https://cdn.example.com', // for unsigned public URLs
1628
- }))
1684
+ app.use(
1685
+ s3({
1686
+ bucket: 'my-app',
1687
+ region: 'us-east-1',
1688
+ endpoint: process.env.S3_URL, // MinIO / R2 / AWS
1689
+ forcePathStyle: true, // required for MinIO
1690
+ credentials: {
1691
+ accessKeyId: process.env.S3_ACCESS_KEY,
1692
+ secretAccessKey: process.env.S3_SECRET_KEY,
1693
+ },
1694
+ publicUrl: 'https://cdn.example.com', // for unsigned public URLs
1695
+ }),
1696
+ )
1629
1697
  ```
1630
1698
 
1631
1699
  Injects `ctx.s3` with methods for S3-compatible object storage.
@@ -1654,14 +1722,14 @@ const publicUrl = await ctx.s3.url('images/logo.png', { expiresIn: 0 })
1654
1722
  const keys = await ctx.s3.list('images/')
1655
1723
  ```
1656
1724
 
1657
- | Option | Type | Default | Description |
1658
- |--------|------|---------|-------------|
1659
- | `bucket` | `string` | — | **Required.** S3 bucket name |
1660
- | `region` | `string` | `'us-east-1'` | AWS region |
1661
- | `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
1662
- | `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
1663
- | `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
1664
- | `publicUrl` | `string` | — | Base URL for unsigned public URLs via `url(key, { expiresIn: 0 })` |
1725
+ | Option | Type | Default | Description |
1726
+ | ---------------- | ---------------------------------- | ------------- | ------------------------------------------------------------------ |
1727
+ | `bucket` | `string` | — | **Required.** S3 bucket name |
1728
+ | `region` | `string` | `'us-east-1'` | AWS region |
1729
+ | `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
1730
+ | `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
1731
+ | `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
1732
+ | `publicUrl` | `string` | — | Base URL for unsigned public URLs via `url(key, { expiresIn: 0 })` |
1665
1733
 
1666
1734
  Credentials can be omitted to use AWS environment variables (`AWS_ACCESS_KEY_ID`,
1667
1735
  `AWS_SECRET_ACCESS_KEY`) or IAM roles (EC2, ECS, Lambda).
@@ -1688,32 +1756,42 @@ minio:
1688
1756
  command: server /data
1689
1757
  ```
1690
1758
 
1691
-
1692
1759
  ### seo [β] + seoMiddleware [α] [API]
1693
1760
 
1694
1761
  ```ts
1695
- app.use('/', seo({ baseUrl: 'https://example.com', robots: [{ userAgent: '*', allow: '/' }], sitemap: { urls: [{ loc: '/' }] } }))
1762
+ app.use(
1763
+ '/',
1764
+ seo({
1765
+ baseUrl: 'https://example.com',
1766
+ robots: [{ userAgent: '*', allow: '/' }],
1767
+ sitemap: { urls: [{ loc: '/' }] },
1768
+ }),
1769
+ )
1696
1770
  // GET /robots.txt, GET /sitemap.xml
1697
1771
 
1698
- app.use(seoMiddleware({ headers: { 'X-Robots-Tag': (path) => path.startsWith('/admin') ? 'noindex' : undefined } }))
1772
+ app.use(
1773
+ seoMiddleware({
1774
+ headers: { 'X-Robots-Tag': (path) => (path.startsWith('/admin') ? 'noindex' : undefined) },
1775
+ }),
1776
+ )
1699
1777
  ```
1700
1778
 
1701
1779
  Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
1702
1780
 
1703
- | Option | Type | Default | Description |
1704
- |--------|------|---------|-------------|
1705
- | `baseUrl` | `string` | — | Base URL for sitemap URLs |
1706
- | `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
1707
- | `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
1708
- | `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
1781
+ | Option | Type | Default | Description |
1782
+ | --------- | ------------------ | ---------------------------------- | ----------------------------------------------- |
1783
+ | `baseUrl` | `string` | — | Base URL for sitemap URLs |
1784
+ | `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
1785
+ | `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
1786
+ | `headers` | `SeoHeadersConfig` | — | Response headers (e.g. `X-Robots-Tag`) |
1709
1787
 
1710
1788
  ### session [α] [Security]
1711
1789
 
1712
1790
  Cookie-based server-side session management with memory and Redis stores.
1713
1791
 
1714
1792
  ```ts
1715
- app.use(session()) // in-memory store (default)
1716
- app.use(session({ store: 'redis', redis: ctx.redis })) // Redis store
1793
+ app.use(session()) // in-memory store (default)
1794
+ app.use(session({ store: 'redis', redis: ctx.redis })) // Redis store
1717
1795
  app.use(session({ store: 'redis', redis, ttl: 30 * 60_000, cookieName: 'sid' }))
1718
1796
 
1719
1797
  app.get('/login', async (req, ctx) => {
@@ -1724,7 +1802,7 @@ app.get('/login', async (req, ctx) => {
1724
1802
  })
1725
1803
 
1726
1804
  app.get('/logout', async (req, ctx) => {
1727
- ctx.session.destroy() // or ctx.session = null
1805
+ ctx.session.destroy() // or ctx.session = null
1728
1806
  return Response.json({ ok: true })
1729
1807
  })
1730
1808
 
@@ -1734,19 +1812,19 @@ app.get('/logout', async (req, ctx) => {
1734
1812
  // Session mutations are auto-detected on property set/delete
1735
1813
  ```
1736
1814
 
1737
- | Option | Type | Default | Description |
1738
- |--------|------|---------|-------------|
1739
- | `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
1740
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1741
- | `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
1742
- | `cookieName` | `string` | `'__session'` | Cookie name |
1743
- | `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
1744
- | `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
1745
- | `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
1746
- | `cookie.path` | `string` | `'/'` | Cookie path |
1747
- | `cookie.domain` | `string` | — | Cookie domain |
1748
- | `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
1749
- | `rotateInterval` | `number` | `900000` (15min) | Auto-rotate session ID to prevent fixation attacks. Set `0` to disable |
1815
+ | Option | Type | Default | Description |
1816
+ | ----------------- | ------------------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------- |
1817
+ | `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
1818
+ | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1819
+ | `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
1820
+ | `cookieName` | `string` | `'__session'` | Cookie name |
1821
+ | `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
1822
+ | `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
1823
+ | `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
1824
+ | `cookie.path` | `string` | `'/'` | Cookie path |
1825
+ | `cookie.domain` | `string` | — | Cookie domain |
1826
+ | `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
1827
+ | `rotateInterval` | `number` | `900000` (15min) | Auto-rotate session ID to prevent fixation attacks. Set `0` to disable |
1750
1828
 
1751
1829
  When `secret` is set, the cookie value is signed with HMAC-SHA256:
1752
1830
  `uuid.base64url(hmac)`. Tampered cookies are rejected and treated as new
@@ -1761,7 +1839,7 @@ from the store. Rotation happens transparently on the next request after
1761
1839
  ```ts
1762
1840
  import { MemoryStore, RedisStore } from 'weifuwu'
1763
1841
 
1764
- const mem = new MemoryStore() // auto-cleanup every 60s
1842
+ const mem = new MemoryStore() // auto-cleanup every 60s
1765
1843
  await mem.set('sid', { userId: 1 }, 86400000)
1766
1844
  mem.close()
1767
1845
 
@@ -1800,14 +1878,14 @@ app.use('/', ssr({ dir: './ui' }))
1800
1878
  └── lib/ ← utilities (does not affect routing)
1801
1879
  ```
1802
1880
 
1803
- | Location | Route |
1804
- |----------|-------|
1805
- | `app/page.tsx` | `GET /` |
1806
- | `app/[param]/page.tsx` | `GET /:param` |
1807
- | `app/layout.tsx` | Root layout (wraps all pages in its subtree) |
1808
- | `app/not-found.tsx` | 404 fallback for that subtree |
1809
- | `app/error.tsx` | Error boundary for that subtree |
1810
- | `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
1881
+ | Location | Route |
1882
+ | ---------------------- | -------------------------------------------------------- |
1883
+ | `app/page.tsx` | `GET /` |
1884
+ | `app/[param]/page.tsx` | `GET /:param` |
1885
+ | `app/layout.tsx` | Root layout (wraps all pages in its subtree) |
1886
+ | `app/not-found.tsx` | 404 fallback for that subtree |
1887
+ | `app/error.tsx` | Error boundary for that subtree |
1888
+ | `app/globals.css` | Tailwind CSS entry (compiled via `@tailwindcss/postcss`) |
1811
1889
 
1812
1890
  **How hydration works:**
1813
1891
 
@@ -1824,7 +1902,7 @@ app.use('/', ssr({ dir: './ui' }))
1824
1902
 
1825
1903
  ```ts
1826
1904
  // Multiple independent SSR directories
1827
- app.use('/', ssr({ dir: './www' }))
1905
+ app.use('/', ssr({ dir: './www' }))
1828
1906
  app.use('/admin', ssr({ dir: './admin' }))
1829
1907
 
1830
1908
  // API routes coexist normally
@@ -1840,31 +1918,35 @@ Multi-tenant BaaS with dynamic table API and GraphQL.
1840
1918
  ```ts
1841
1919
  const t = tenant({ pg, usersTable: '_users' })
1842
1920
  await t.migrate()
1843
- app.use('/api', t.middleware()) // → ctx.tenant
1844
- app.use('/api', t) // dynamic CRUD
1921
+ app.use('/api', t.middleware()) // → ctx.tenant
1922
+ app.use('/api', t) // dynamic CRUD
1845
1923
  app.use('/graphql', t.graphql()) // dynamic GraphQL
1846
1924
  ```
1847
1925
 
1848
- | Option | Type | Default | Description |
1849
- |--------|------|---------|-------------|
1850
- | `pg` | `object` | — | PostgreSQL client |
1851
- | `usersTable` | `string` | — | Users table name for tenant membership lookup |
1926
+ | Option | Type | Default | Description |
1927
+ | ------------ | -------- | ------- | --------------------------------------------- |
1928
+ | `pg` | `object` | — | PostgreSQL client |
1929
+ | `usersTable` | `string` | — | Users table name for tenant membership lookup |
1852
1930
 
1853
1931
  ### upload [α] [DevTools]
1854
1932
 
1855
1933
  ```ts
1856
- app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }), (req, ctx) => {
1857
- // ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
1858
- // Multiple files with same field name array
1859
- // ctx.parsed.fields.title 'hello'
1860
- })
1934
+ app.post(
1935
+ '/upload',
1936
+ upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }),
1937
+ (req, ctx) => {
1938
+ // ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
1939
+ // Multiple files with same field name → array
1940
+ // ctx.parsed.fields.title → 'hello'
1941
+ },
1942
+ )
1861
1943
  ```
1862
1944
 
1863
- | Option | Type | Default | Description |
1864
- |--------|------|---------|-------------|
1865
- | `dir` | `string` | — | Write files to disk (omit for in-memory) |
1866
- | `maxFileSize` | `number` | — | Max bytes per file |
1867
- | `allowedTypes` | `string[]` | — | Allowed MIME types |
1945
+ | Option | Type | Default | Description |
1946
+ | -------------- | ---------- | ------- | ---------------------------------------- |
1947
+ | `dir` | `string` | — | Write files to disk (omit for in-memory) |
1948
+ | `maxFileSize` | `number` | — | Max bytes per file |
1949
+ | `allowedTypes` | `string[]` | — | Allowed MIME types |
1868
1950
 
1869
1951
  ### user [β] [Security]
1870
1952
 
@@ -1874,7 +1956,8 @@ Authentication: register, login, JWT, OAuth2 服务端, 社会化登录.
1874
1956
  const u = user({
1875
1957
  pg,
1876
1958
  jwtSecret: process.env.JWT_SECRET!,
1877
- oauthLogin: { // 可选 — 社会化登录
1959
+ oauthLogin: {
1960
+ // 可选 — 社会化登录
1878
1961
  providers: {
1879
1962
  github: { clientId: '...', clientSecret: '...' },
1880
1963
  google: { clientId: '...', clientSecret: '...' },
@@ -1882,26 +1965,26 @@ const u = user({
1882
1965
  },
1883
1966
  })
1884
1967
  await u.migrate()
1885
- app.use(u) // POST /register, POST /login
1886
- app.use(u.middleware()) // ctx.user
1968
+ app.use(u) // POST /register, POST /login
1969
+ app.use(u.middleware()) // ctx.user
1887
1970
  // GET /auth/github, GET /auth/github/callback (如配置 oauthLogin)
1888
1971
  ```
1889
1972
 
1890
- | Option | Type | Default | Description |
1891
- |--------|------|---------|-------------|
1892
- | `pg` | `object` | — | PostgreSQL client |
1893
- | `jwtSecret` | `string` | — | JWT signing secret |
1894
- | `table` | `string` | `'_users'` | Users table name |
1895
- | `expiresIn` | `string` | `'24h'` | JWT expiration |
1896
- | `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
1897
- | `oauthLogin` | `object` | — | 社会化登录: `{ providers: Record<string, OAuthProviderConfig>, redirectUrl? }` |
1973
+ | Option | Type | Default | Description |
1974
+ | ------------ | -------- | ---------- | ------------------------------------------------------------------------------ |
1975
+ | `pg` | `object` | — | PostgreSQL client |
1976
+ | `jwtSecret` | `string` | — | JWT signing secret |
1977
+ | `table` | `string` | `'_users'` | Users table name |
1978
+ | `expiresIn` | `string` | `'24h'` | JWT expiration |
1979
+ | `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
1980
+ | `oauthLogin` | `object` | — | 社会化登录: `{ providers: Record<string, OAuthProviderConfig>, redirectUrl? }` |
1898
1981
 
1899
- | Method | Description |
1900
- |--------|-------------|
1901
- | `.register(data)` | Register a new user programmatically |
1902
- | `.login(data)` | Log in programmatically |
1903
- | `.verify(token)` | Verify JWT token |
1904
- | `.middleware()` | JWT verify middleware — sets `ctx.user` |
1982
+ | Method | Description |
1983
+ | ----------------- | --------------------------------------- |
1984
+ | `.register(data)` | Register a new user programmatically |
1985
+ | `.login(data)` | Log in programmatically |
1986
+ | `.verify(token)` | Verify JWT token |
1987
+ | `.middleware()` | JWT verify middleware — sets `ctx.user` |
1905
1988
 
1906
1989
  ### permissions [α] — RBAC [Security]
1907
1990
 
@@ -1915,11 +1998,14 @@ await perm.migrate()
1915
1998
  await perm.assignRole(userId, 'admin')
1916
1999
  await perm.grantPermission('admin', 'posts:create')
1917
2000
  await perm.grantPermission('admin', 'posts:edit')
1918
- await perm.grantPermission('admin', '*') // wildcard — all permissions
2001
+ await perm.grantPermission('admin', '*') // wildcard — all permissions
1919
2002
 
1920
2003
  // Use as middleware
1921
- app.use((req, ctx, next) => { ctx.user = { id: userId }; return next(req, ctx) })
1922
- app.use(perm) // ctx.roles, ctx.permissions
2004
+ app.use((req, ctx, next) => {
2005
+ ctx.user = { id: userId }
2006
+ return next(req, ctx)
2007
+ })
2008
+ app.use(perm) // → ctx.roles, ctx.permissions
1923
2009
 
1924
2010
  // Route guards
1925
2011
  app.get('/admin', perm.requireRole('admin'), adminHandler)
@@ -1934,26 +2020,26 @@ app.get('/posts/:id', async (req, ctx) => {
1934
2020
  })
1935
2021
  ```
1936
2022
 
1937
- | Option | Type | Default | Description |
1938
- |--------|------|---------|-------------|
1939
- | `pg` | `object` | — | PostgreSQL client |
1940
- | `prefix` | `string` | `''` | Table prefix (e.g. `'myapp'` → `myapp_roles`) |
2023
+ | Option | Type | Default | Description |
2024
+ | -------- | -------- | ------- | --------------------------------------------- |
2025
+ | `pg` | `object` | — | PostgreSQL client |
2026
+ | `prefix` | `string` | `''` | Table prefix (e.g. `'myapp'` → `myapp_roles`) |
1941
2027
 
1942
- | Method | Description |
1943
- |--------|-------------|
1944
- | `.assignRole(userId, role)` | Assign role to user (creates role if missing) |
1945
- | `.removeRole(userId, role)` | Remove role from user |
1946
- | `.grantPermission(role, permission)` | Grant permission to role |
1947
- | `.revokePermission(role, permission)` | Revoke permission from role |
1948
- | `.getUserRoles(userId)` | List user's roles |
1949
- | `.getUserPermissions(userId)` | List user's permissions (union of all roles) |
1950
- | `.requireRole(...roles)` | Middleware — rejects if user lacks any of the roles |
1951
- | `.requirePermission(...perms)` | Middleware — rejects if user lacks any permission |
1952
- | `.migrate()` | Create tables |
2028
+ | Method | Description |
2029
+ | ------------------------------------- | --------------------------------------------------- |
2030
+ | `.assignRole(userId, role)` | Assign role to user (creates role if missing) |
2031
+ | `.removeRole(userId, role)` | Remove role from user |
2032
+ | `.grantPermission(role, permission)` | Grant permission to role |
2033
+ | `.revokePermission(role, permission)` | Revoke permission from role |
2034
+ | `.getUserRoles(userId)` | List user's roles |
2035
+ | `.getUserPermissions(userId)` | List user's permissions (union of all roles) |
2036
+ | `.requireRole(...roles)` | Middleware — rejects if user lacks any of the roles |
2037
+ | `.requirePermission(...perms)` | Middleware — rejects if user lacks any permission |
2038
+ | `.migrate()` | Create tables |
1953
2039
 
1954
2040
  ### validate [α] [DevTools]
1955
2041
 
1956
- ```ts
2042
+ ````ts
1957
2043
  import { z } from 'zod'
1958
2044
  const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
1959
2045
  app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string().optional() }) }), (req, ctx) => {
@@ -1976,14 +2062,14 @@ app.post('/contact', validate(), (req, ctx) => {
1976
2062
 
1977
2063
  // Or validate with Zod
1978
2064
  app.post('/contact', validate({ body: z.object({ email: z.string().email() }) }), handler)
1979
- ```
2065
+ ````
1980
2066
 
1981
- | Option | Type | Default | Description |
1982
- |--------|------|---------|-------------|
1983
- | `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
1984
- | `query` | `ZodSchema` | — | Query validation schema |
1985
- | `params` | `ZodSchema` | — | URL params validation schema |
1986
- | `headers` | `ZodSchema` | — | Header validation schema |
2067
+ | Option | Type | Default | Description |
2068
+ | --------- | ----------- | ------- | ------------------------------------- |
2069
+ | `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
2070
+ | `query` | `ZodSchema` | — | Query validation schema |
2071
+ | `params` | `ZodSchema` | — | URL params validation schema |
2072
+ | `headers` | `ZodSchema` | — | Header validation schema |
1987
2073
 
1988
2074
  ### webhook [β] [API]
1989
2075
 
@@ -2013,14 +2099,14 @@ wh.on('*', (event) => {
2013
2099
  })
2014
2100
  ```
2015
2101
 
2016
- | Option | Type | Default | Description |
2017
- |--------|------|---------|-------------|
2018
- | `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
2019
- | `github` | `PlatformConfig` | — | GitHub webhook config |
2020
- | `slack` | `PlatformConfig` | — | Slack webhook config |
2021
- | `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
2022
- | `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
2023
- | `idempotencyTTL` | `number` | `3600000` | Dedup TTL (ms) |
2102
+ | Option | Type | Default | Description |
2103
+ | ------------------ | ------------------------ | --------- | ----------------------------------- |
2104
+ | `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
2105
+ | `github` | `PlatformConfig` | — | GitHub webhook config |
2106
+ | `slack` | `PlatformConfig` | — | Slack webhook config |
2107
+ | `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
2108
+ | `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
2109
+ | `idempotencyTTL` | `number` | `3600000` | Dedup TTL (ms) |
2024
2110
 
2025
2111
  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.
2026
2112
 
@@ -2028,11 +2114,12 @@ Built-in verifiers handle HMAC-SHA256, timestamp validation (Slack's 5-min windo
2028
2114
 
2029
2115
  ```tsx
2030
2116
  import { Link, navigate, useNavigate, useNavigating } from 'weifuwu/react'
2031
-
2032
- <Link href="/about" prefetch>About</Link> // client-side nav + prefetch on hover/visible
2033
- const n = useNavigate() // hook: n('/contact')
2034
- navigate('/contact') // bare function (no hook needed)
2035
- const loading = useNavigating() // reactive loading state
2117
+ ;<Link href="/about" prefetch>
2118
+ About
2119
+ </Link> // client-side nav + prefetch on hover/visible
2120
+ const n = useNavigate() // hook: n('/contact')
2121
+ navigate('/contact') // bare function (no hook needed)
2122
+ const loading = useNavigating() // reactive loading state
2036
2123
  ```
2037
2124
 
2038
2125
  `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.
@@ -2089,11 +2176,11 @@ function LangSwitch() {
2089
2176
  }
2090
2177
  ```
2091
2178
 
2092
- | Return | Description |
2093
- |--------|-------------|
2094
- | `locale` | Current locale string (from `ctx.i18n.locale`) |
2179
+ | Return | Description |
2180
+ | ------------------- | ----------------------------------------------------- |
2181
+ | `locale` | Current locale string (from `ctx.i18n.locale`) |
2095
2182
  | `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
2096
- | `t` | Translate a key using loaded locale messages |
2183
+ | `t` | Translate a key using loaded locale messages |
2097
2184
 
2098
2185
  ```tsx
2099
2186
  import { useTheme } from 'weifuwu/react'
@@ -2101,8 +2188,8 @@ function ThemeToggle() {
2101
2188
  const { theme, resolvedTheme, setTheme } = useTheme()
2102
2189
  return (
2103
2190
  <>
2104
- <span>Current: {resolvedTheme}</span> {/* 'dark' | 'light' — never 'system' */}
2105
- <select value={theme} onChange={e => setTheme(e.target.value)}>
2191
+ <span>Current: {resolvedTheme}</span> {/* 'dark' | 'light' — never 'system' */}
2192
+ <select value={theme} onChange={(e) => setTheme(e.target.value)}>
2106
2193
  <option value="light">☀ Light</option>
2107
2194
  <option value="dark">🌙 Dark</option>
2108
2195
  <option value="system">💻 System</option>
@@ -2112,11 +2199,11 @@ function ThemeToggle() {
2112
2199
  }
2113
2200
  ```
2114
2201
 
2115
- | Return | Description |
2116
- |--------|-------------|
2117
- | `theme` | Raw preference (`'light'` \| `'dark'` \| `'system'`) |
2118
- | `resolvedTheme` | Resolved value (`'light'` \| `'dark'`) — `'system'` → matchMedia |
2119
- | `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`) |
2202
+ | Return | Description |
2203
+ | ----------------- | ---------------------------------------------------------------- |
2204
+ | `theme` | Raw preference (`'light'` \| `'dark'` \| `'system'`) |
2205
+ | `resolvedTheme` | Resolved value (`'light'` \| `'dark'`) — `'system'` → matchMedia |
2206
+ | `setTheme(theme)` | Switch theme (calls `navigate('/__theme/' + theme)`) |
2120
2207
 
2121
2208
  **`applyTheme(theme)`** — DOM-only theme application. Sets `data-theme` on `<html>`, registers `matchMedia` listener for `'system'`. Used by the interceptor; exported for custom scenarios.
2122
2209
 
@@ -2126,7 +2213,13 @@ function ThemeToggle() {
2126
2213
  import { useLoaderData } from 'weifuwu/react'
2127
2214
  function Page() {
2128
2215
  const data = useLoaderData<{ posts: Post[] }>()
2129
- return <ul>{data.posts.map(p => <li key={p.id}>{p.title}</li>)}</ul>
2216
+ return (
2217
+ <ul>
2218
+ {data.posts.map((p) => (
2219
+ <li key={p.id}>{p.title}</li>
2220
+ ))}
2221
+ </ul>
2222
+ )
2130
2223
  }
2131
2224
  ```
2132
2225
 
@@ -2152,7 +2245,7 @@ app.use(flash())
2152
2245
 
2153
2246
  // Read flash
2154
2247
  app.get('/', (req, ctx) => {
2155
- const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
2248
+ const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
2156
2249
  })
2157
2250
 
2158
2251
  // Set flash + redirect
@@ -2174,8 +2267,8 @@ function Toast() {
2174
2267
  }
2175
2268
  ```
2176
2269
 
2177
- | Option | Type | Default | Description |
2178
- |--------|------|---------|-------------|
2270
+ | Option | Type | Default | Description |
2271
+ | ------ | -------- | --------- | ----------- |
2179
2272
  | `name` | `string` | `'flash'` | Cookie name |
2180
2273
 
2181
2274
  ### Dev mode [δ] [Client]
@@ -2193,7 +2286,17 @@ Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically regi
2193
2286
  ## AI
2194
2287
 
2195
2288
  ```ts
2196
- import { openai, streamText, generateText, streamObject, generateObject, tool, embed, embedMany, aiProvider } from 'weifuwu'
2289
+ import {
2290
+ openai,
2291
+ streamText,
2292
+ generateText,
2293
+ streamObject,
2294
+ generateObject,
2295
+ tool,
2296
+ embed,
2297
+ embedMany,
2298
+ aiProvider,
2299
+ } from 'weifuwu'
2197
2300
  import { runWorkflow } from 'weifuwu'
2198
2301
 
2199
2302
  const provider = aiProvider()
@@ -2216,23 +2319,23 @@ app.post('/ask', async (req, ctx) => {
2216
2319
  })
2217
2320
  ```
2218
2321
 
2219
- | Option | Type | Default | Description |
2220
- |--------|------|---------|-------------|
2221
- | `baseURL` | `string` | `OPENAI_BASE_URL` env or `http://localhost:11434/v1` | API base URL |
2222
- | `apiKey` | `string` | `OPENAI_API_KEY` env or `'ollama'` | API key |
2223
- | `model` | `string` | `OPENAI_MODEL` env or `'qwen3:0.6b'` | Chat model name |
2224
- | `embeddingModel` | `string` | `OPENAI_EMBEDDING_MODEL` env or `'qwen3-embedding:0.6b'` | Embedding model name |
2225
- | `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024` | Vector dimension |
2226
-
2227
- | Method | Description |
2228
- |--------|-------------|
2229
- | `.model(name?)` | Get `LanguageModel` instance |
2230
- | `.embeddingModel(name?)` | Get `EmbeddingModel` instance |
2231
- | `.embed(text)` | Embed single text → `Promise<number[]>` |
2232
- | `.embedMany(texts)` | Batch embed → `Promise<number[][]>` |
2233
- | `.generateText(params)` | Generate text (model auto-injected) |
2234
- | `.streamText(params)` | Stream text (model auto-injected) |
2235
- | `.dimension` | Configured embedding dimension |
2322
+ | Option | Type | Default | Description |
2323
+ | -------------------- | -------- | -------------------------------------------------------- | -------------------- |
2324
+ | `baseURL` | `string` | `OPENAI_BASE_URL` env or `http://localhost:11434/v1` | API base URL |
2325
+ | `apiKey` | `string` | `OPENAI_API_KEY` env or `'ollama'` | API key |
2326
+ | `model` | `string` | `OPENAI_MODEL` env or `'qwen3:0.6b'` | Chat model name |
2327
+ | `embeddingModel` | `string` | `OPENAI_EMBEDDING_MODEL` env or `'qwen3-embedding:0.6b'` | Embedding model name |
2328
+ | `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024` | Vector dimension |
2329
+
2330
+ | Method | Description |
2331
+ | ------------------------ | --------------------------------------- |
2332
+ | `.model(name?)` | Get `LanguageModel` instance |
2333
+ | `.embeddingModel(name?)` | Get `EmbeddingModel` instance |
2334
+ | `.embed(text)` | Embed single text → `Promise<number[]>` |
2335
+ | `.embedMany(texts)` | Batch embed → `Promise<number[][]>` |
2336
+ | `.generateText(params)` | Generate text (model auto-injected) |
2337
+ | `.streamText(params)` | Stream text (model auto-injected) |
2338
+ | `.dimension` | Configured embedding dimension |
2236
2339
 
2237
2340
  ### DAG Workflow [AI]
2238
2341
 
@@ -2246,12 +2349,12 @@ const wf = runWorkflow({ tools, provider })
2246
2349
  const wf = runWorkflow({ tools, model: openai('gpt-4o') })
2247
2350
  ```
2248
2351
 
2249
- | Option | Type | Default | Description |
2250
- |--------|------|---------|-------------|
2251
- | `tools` | `object` | — | Registered tool definitions |
2252
- | `provider` | `AIProvider` | — | AI provider (uses `provider.model()` for LLM-generated workflow) |
2253
- | `model` | `LanguageModel` | — | Explicit model (overrides provider) |
2254
- | `maxSteps` | `number` | `200` | Max execution steps |
2352
+ | Option | Type | Default | Description |
2353
+ | ---------- | --------------- | ------- | ---------------------------------------------------------------- |
2354
+ | `tools` | `object` | — | Registered tool definitions |
2355
+ | `provider` | `AIProvider` | — | AI provider (uses `provider.model()` for LLM-generated workflow) |
2356
+ | `model` | `LanguageModel` | — | Explicit model (overrides provider) |
2357
+ | `maxSteps` | `number` | `200` | Max execution steps |
2255
2358
 
2256
2359
  ---
2257
2360
 
@@ -2259,7 +2362,10 @@ const wf = runWorkflow({ tools, model: openai('gpt-4o') })
2259
2362
 
2260
2363
  ```ts
2261
2364
  import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
2262
- async function* events() { yield formatSSE('chat', 'Hello'); yield formatSSE('chat', 'World') }
2365
+ async function* events() {
2366
+ yield formatSSE('chat', 'Hello')
2367
+ yield formatSSE('chat', 'World')
2368
+ }
2263
2369
  app.get('/stream', (req, ctx) => createSSEStream(events()))
2264
2370
  ```
2265
2371