weifuwu 0.24.0 → 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 (76) hide show
  1. package/README.md +970 -756
  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/agent/types.d.ts +2 -1
  14. package/dist/ai/workflow.d.ts +1 -1
  15. package/dist/ai-sdk.d.ts +1 -1
  16. package/dist/analytics.d.ts +2 -2
  17. package/dist/cache.d.ts +4 -4
  18. package/dist/cli.js +135 -97
  19. package/dist/cookie.d.ts +24 -0
  20. package/dist/cors.d.ts +2 -2
  21. package/dist/deploy/types.d.ts +2 -2
  22. package/dist/fts.d.ts +5 -5
  23. package/dist/helmet.d.ts +2 -2
  24. package/dist/hub.d.ts +2 -1
  25. package/dist/iii/index.d.ts +1 -1
  26. package/dist/iii/register-worker.d.ts +1 -1
  27. package/dist/iii/types.d.ts +4 -4
  28. package/dist/index.d.ts +5 -5
  29. package/dist/index.js +905 -442
  30. package/dist/kb/types.d.ts +8 -0
  31. package/dist/live.d.ts +2 -3
  32. package/dist/logdb/rest.d.ts +1 -1
  33. package/dist/logdb/types.d.ts +2 -1
  34. package/dist/mailer.d.ts +3 -2
  35. package/dist/messager/agent.d.ts +2 -2
  36. package/dist/messager/rest.d.ts +3 -3
  37. package/dist/messager/types.d.ts +2 -1
  38. package/dist/messager/ws.d.ts +3 -3
  39. package/dist/opencode/index.d.ts +1 -1
  40. package/dist/opencode/permissions.d.ts +1 -1
  41. package/dist/opencode/run.d.ts +1 -1
  42. package/dist/opencode/session.d.ts +9 -9
  43. package/dist/opencode/tools/web.d.ts +1 -1
  44. package/dist/opencode/types.d.ts +2 -1
  45. package/dist/opencode/ws.d.ts +1 -2
  46. package/dist/permissions.d.ts +4 -4
  47. package/dist/postgres/module.d.ts +5 -4
  48. package/dist/postgres/schema/index.d.ts +1 -1
  49. package/dist/postgres/schema/table.d.ts +22 -20
  50. package/dist/postgres/types.d.ts +6 -6
  51. package/dist/queue/types.d.ts +3 -3
  52. package/dist/rate-limit.d.ts +1 -1
  53. package/dist/react.d.ts +1 -1
  54. package/dist/react.js +141 -96
  55. package/dist/redis/types.d.ts +2 -2
  56. package/dist/router.d.ts +10 -10
  57. package/dist/seo.d.ts +2 -2
  58. package/dist/serve.d.ts +1 -1
  59. package/dist/session.d.ts +8 -5
  60. package/dist/tailwind.d.ts +9 -0
  61. package/dist/tenant/graphql.d.ts +2 -2
  62. package/dist/tenant/index.d.ts +1 -1
  63. package/dist/tenant/rest.d.ts +2 -2
  64. package/dist/tenant/types.d.ts +3 -3
  65. package/dist/test-utils.d.ts +3 -3
  66. package/dist/types.d.ts +8 -0
  67. package/dist/upload.d.ts +4 -2
  68. package/dist/user/index.d.ts +1 -1
  69. package/dist/user/oauth-login.d.ts +2 -2
  70. package/dist/user/types.d.ts +2 -2
  71. package/dist/vendor.d.ts +4 -0
  72. package/opencode/ui/app/globals.css +1 -1
  73. package/opencode/ui/app/layout.tsx +2 -3
  74. package/opencode/ui/app/page.tsx +302 -73
  75. package/package.json +33 -10
  76. 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,41 +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
- | `csrfToken` | `csrf()` | `string` | CSRF token |
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
- | `session` | `session()` | `Session` | Session data object |
207
- | `user` | `auth()` / `user().middleware()` | `{ id?: string }` | Authenticated user |
208
- | `permissions` | `permissions()` | `{ roles, permissions }` | RBAC roles & permissions sets |
209
- | `theme` | `theme()` | `{ value, set }` | Current theme + switcher |
210
- | `i18n` | `i18n()` | `{ locale, t, set }` | Locale, translation, switcher |
211
- | `flash` | `flash()` | `{ value, set }` | Flash message + setter |
212
- | `tailwind` | `tailwindContext()` | `{ css, url }` | Compiled Tailwind CSS |
213
- | `tenant` | `tenant()` | `TenantContext` | Current tenant info |
214
- | `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
215
- | `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
216
- | `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
217
- | `mountPath` | `Router` | `string` | Sub-router mount path |
218
- | `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 |
219
242
 
220
243
  ### Type-Safe Context
221
244
 
@@ -223,13 +246,13 @@ Middleware-injected properties are **automatically typed** through chained `use(
223
246
 
224
247
  ```ts
225
248
  const app = new Router()
226
- .use(csrf()) // → Router<Context & { csrfToken: string }>
227
- .use(requestId()) // → Router<Context & { csrfToken, requestId }>
228
- .use(postgres()) // → Router<Context & { csrfToken, 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 }>
229
252
 
230
253
  app.get('/me', (_req, ctx) => {
231
- ctx.csrfToken // ✅ string (IDE autocomplete)
232
- ctx.requestId // ✅ string
254
+ ctx.csrf.token // ✅ string (IDE autocomplete)
255
+ ctx.requestId // ✅ string
233
256
  ctx.sql`SELECT 1` // ✅ Sql<{}>
234
257
  })
235
258
  ```
@@ -242,43 +265,44 @@ Each module exports an `XxxInjected` type (e.g. `PostgresInjected`, `UserInjecte
242
265
 
243
266
  All modules follow one of **4 patterns** — learn these and you know every module.
244
267
 
245
- | Pattern | How to mount | Example |
246
- |---------|-------------|---------|
247
- | `[α]` | `app.use(mod())` | `compress()`, `theme()`, `postgres()` |
248
- | `[β]` | `app.use('/path', mod())` | `health()`, `ssr({dir})`, `graphql(handler)`, `user()` |
249
- | `[γ]` | Import and call directly | `mailer()`, `fts`, `cron-utils` |
250
- | `[δ]` | `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()` |
251
274
 
252
275
  ### Pattern α — Middleware
253
276
 
254
277
  ```ts
255
- app.use(compress()) // basic
256
- const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
278
+ app.use(compress()) // basic
279
+ const pg = postgres() // with extras: .sql, .table, .migrate(), .close()
257
280
  app.use(pg)
258
- app.use(rateLimit({ max: 100 })) // with .close()
281
+ app.use(rateLimit({ max: 100 })) // with .close()
259
282
  ```
260
283
 
261
284
  ### Pattern β — Router
262
285
 
263
286
  ```ts
264
- app.use('/health', health()) // with path
287
+ app.use('/health', health()) // with path
265
288
  app.use('/graphql', graphql(handler))
266
- app.use('/logs', logdb({ pg })) // with .log(), .migrate()
267
- 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()
268
291
  app.ws('/ws', messager({ pg }).wsHandler())
269
292
  ```
270
293
 
271
294
  β modules that need **separate middleware** use `.middleware()`. Most can auto-register both middleware and routes in one call:
295
+
272
296
  ```ts
273
- app.use(theme()) // auto: middleware + /__theme/:value
274
- 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
275
299
  app.use(analytics({ pg })) // auto: middleware + /__analytics
276
- app.use(auth) // auto: middleware + /register, /login (user())
300
+ app.use(auth) // auto: middleware + /register, /login (user())
277
301
 
278
302
  // Explicit form when more control is needed:
279
303
  const a = analytics()
280
- app.use(a.middleware()) // tracking only
281
- app.use('/', a) // dashboard at custom path
304
+ app.use(a.middleware()) // tracking only
305
+ app.use('/', a) // dashboard at custom path
282
306
  ```
283
307
 
284
308
  ### Pattern γ — Standalone
@@ -291,7 +315,7 @@ import { mailer, cronNext, fts } from 'weifuwu'
291
315
  const email = mailer({ transport: 'smtp://...', from: 'noreply@example.com' })
292
316
  await email.send({ to: 'user@test.com', subject: 'Hello', text: 'Body' })
293
317
 
294
- 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
295
319
  ```
296
320
 
297
321
  ### Pattern δ — Client-side
@@ -347,38 +371,54 @@ graph TD
347
371
 
348
372
  ## Quick Module Selection
349
373
 
350
- | What do you want to do? | Module | Pattern |
351
- |------------------------|--------|---------|
352
- | **User registration / login** | `user()` | β |
353
- | **Simple token/header auth** | `auth()` | α |
354
- | **JWT verification** | `user().middleware()` | α |
355
- | **Role-based access control** | `permissions()` | α |
356
- | **AI chat / generate / stream** | `ctx.ai.generateText()` / `ctx.ai.streamText()` | α (via `aiProvider()`) |
357
- | **AI agent with knowledge** | `agent()` + `knowledgeBase()` | β |
358
- | **Send email** | `mailer()` | γ |
359
- | **File upload** | `upload()` | α |
360
- | **Object storage (S3/MinIO)** | `s3()` | α |
361
- | **Rate limiting** | `rateLimit()` | α |
362
- | **Response caching** | `cache()` | α |
363
- | **Periodic / delayed jobs** | `queue()` | α |
364
- | **Page view analytics** | `analytics()` | β |
365
- | **Structured logging** | `logdb()` | β |
366
- | **Real-time chat / messager** | `messager()` | β |
367
- | **Full-text search** | `fts` | γ |
368
- | **Theme switching** | `theme()` | α |
369
- | **i18n / localization** | `i18n()` | α |
370
- | **Flash messages** | `flash()` | α |
371
- | **Server-Sent Events** | `createSSEStream()` | γ |
372
- | **GraphQL endpoint** | `graphql()` | β |
373
- | **Webhook receiver** | `webhook()` | β |
374
- | **SSR with React** | `ssr()` | β |
375
- | **Health check** | `health()` | β |
376
- | **SEO (robots.txt, sitemap)** | `seo()` | β |
377
- | **Multi-process deploy** | `deploy()` | γ |
378
- | **Distributed functions (iii)** | `iii()` | β |
379
- | **Multi-tenant BaaS** | `tenant()` | β |
380
- | **Client-side routing** | `useNavigate()`, `<Link>` | δ |
381
- | **WebSocket in React** | `useWebsocket()` | δ |
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()` | — |
382
422
 
383
423
  ---
384
424
 
@@ -397,7 +437,16 @@ app.get('/api', (req, ctx) => {
397
437
  **Structured logging** — `logger({ format: 'json' })` outputs JSON to stderr with `traceId`, `timestamp`, `elapsed_ms`:
398
438
 
399
439
  ```json
400
- {"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
+ }
401
450
  ```
402
451
 
403
452
  Default format is `'short'` (human-readable). `'combined'` includes query strings.
@@ -465,12 +514,12 @@ assert.equal(res.status, 200)
465
514
  assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
466
515
  ```
467
516
 
468
- | Method | Description |
469
- |--------|-------------|
470
- | `app.getReq(path)` `postReq` `putReq` `patchReq` `deleteReq` | Start building a request |
471
- | `.withUser(u)` `.withTenant(t)` `.with(ctx)` | Simulate middleware injection |
472
- | `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
473
- | `.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() }` |
474
523
 
475
524
  ### Database test isolation
476
525
 
@@ -481,7 +530,7 @@ import { createTestDb, withTestDb } from 'weifuwu'
481
530
  const db = await createTestDb()
482
531
  await db.sql`CREATE TABLE users (id SERIAL PRIMARY KEY, name TEXT)`
483
532
  await db.sql`INSERT INTO users (name) VALUES ('Alice')`
484
- await db.destroy() // DROP SCHEMA ... CASCADE
533
+ await db.destroy() // DROP SCHEMA ... CASCADE
485
534
 
486
535
  // Transaction rollback — all changes are rolled back after callback
487
536
  await withTestDb(async (sql) => {
@@ -490,10 +539,10 @@ await withTestDb(async (sql) => {
490
539
  })
491
540
  ```
492
541
 
493
- | Function | Description |
494
- |----------|-------------|
495
- | `createTestDb(opts?)` | Create isolated schema, returns `{ sql, url, schema, destroy }` |
496
- | `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 |
497
546
 
498
547
  Uses `TEST_DATABASE_URL` or `DATABASE_URL`. Automatically skipped in CI if unset.
499
548
 
@@ -518,21 +567,21 @@ await a.addKnowledge(agentId, 'Title', 'some knowledge content')
518
567
  a.run(agentId, { input: 'summarize the data', stream: true })
519
568
  ```
520
569
 
521
- | Option | Type | Default | Description |
522
- |--------|------|---------|-------------|
523
- | `pg` | `object` | — | PostgreSQL client |
524
- | `provider` | `AIProvider` | `aiProvider()` (from env) | AI provider for model & embedding resolution |
525
- | `model` | `object` | — | Explicit AI model (overrides provider) |
526
- | `embeddingModel` | `object` | — | Explicit embedding model (overrides provider) |
527
- | `embeddingDimension` | `number` | `provider.dimension` | Embedding vector dimension |
528
- | `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 |
529
578
 
530
- | Method | Description |
531
- |--------|-------------|
579
+ | Method | Description |
580
+ | ---------------------------------------------- | ------------------------ |
532
581
  | `.run(agentId, { input, stream?, messages? })` | Execute agent with input |
533
- | `.addKnowledge(agentId, title, content)` | Add knowledge document |
534
- | `.migrate()` | DB setup |
535
- | `.close()` | Cleanup |
582
+ | `.addKnowledge(agentId, title, content)` | Add knowledge document |
583
+ | `.migrate()` | DB setup |
584
+ | `.close()` | Cleanup |
536
585
 
537
586
  ### aiStream [β] [AI]
538
587
 
@@ -544,10 +593,10 @@ const chat = await aiStream(async (req) => ({ messages: (await req.json()).messa
544
593
  app.use('/chat', chat)
545
594
  ```
546
595
 
547
- | Param | Type | Description |
548
- |-------|------|-------------|
549
- | `handler` | `(req, ctx) => AIStreamOptions \| Promise<AIStreamOptions>` | Returns AI SDK options (model, messages, schema, etc.) |
550
- | `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 |
551
600
 
552
601
  ### analytics [β] [API]
553
602
 
@@ -556,49 +605,52 @@ In-memory or PostgreSQL page view tracking with built-in dashboard.
556
605
  ```ts
557
606
  const a = analytics()
558
607
  app.use(a.middleware())
559
- 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)
560
609
  ```
561
610
 
562
- | Option | Type | Default | Description |
563
- |--------|------|---------|-------------|
564
- | `pg` | `object` | — | PostgreSQL client for persistence |
565
- | `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 |
566
615
 
567
616
  ```ts
568
617
  // With PostgreSQL
569
618
  const a = analytics({ pg })
570
619
  await a.migrate()
571
620
  app.use(a.middleware())
572
- app.use('/', a) // dashboard routes
621
+ app.use('/', a) // dashboard routes
573
622
  ```
574
623
 
575
624
  ### auth [α] [Security]
576
625
 
577
626
  ```ts
578
- app.use(auth({ token: 'sk-123' })) // static token
579
- 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
580
629
  app.use(auth({ verify: async (token, req) => ({ sub: 'abc' }) })) // custom verify → sets ctx.user
581
630
  app.get('/protected', auth({ proxy: 'http://auth:3000/validate' }), handler)
582
631
 
583
632
  // Session-based auth (must be placed after session() middleware)
584
633
  app.use(session())
585
- app.use(auth({
586
- session: true,
587
- resolveUser: async (userId) => { // load user from DB
588
- const [user] = await sql`SELECT * FROM users WHERE id = ${userId}`
589
- return user ?? null // null destroy stale session
590
- },
591
- }))
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
+ )
592
644
  ```
593
645
 
594
- | Option | Type | Default | Description |
595
- |--------|------|---------|-------------|
596
- | `token` | `string` | — | Static token to match |
597
- | `header` | `string` | `'Authorization'` | Header name |
598
- | `verify` | `(token, req) => object\|null` | — | Verify function, return value sets `ctx.user` |
599
- | `proxy` | `string` | — | Auth service URL to proxy requests to |
600
- | `session` | `boolean` | `false` | Enable session-based auth. Checks `ctx.session.userId` first |
601
- | `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 |
602
654
 
603
655
  When `session: true`, auth checks `ctx.session.userId` before the
604
656
  Authorization header. This lets logged-in users authenticate via their
@@ -608,32 +660,32 @@ if no session userId is present.
608
660
  ### compress [α] [DevTools]
609
661
 
610
662
  ```ts
611
- app.use(compress()) // brotli > gzip > deflate (min 1KB)
612
- 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
613
665
  ```
614
666
 
615
- | Option | Type | Default | Description |
616
- |--------|------|---------|-------------|
617
- | `threshold` | `number` | `1024` | Minimum byte size to compress |
618
- | `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) |
619
671
 
620
672
  ### cors [α] [DevTools]
621
673
 
622
674
  ```ts
623
- app.use(cors()) // allow all
624
- app.use(cors({ origin: ['https://example.com'] })) // whitelist
675
+ app.use(cors()) // allow all
676
+ app.use(cors({ origin: ['https://example.com'] })) // whitelist
625
677
  app.use(cors({ origin: (o) => o.endsWith('.trusted.com') && o }))
626
678
  app.use(cors({ credentials: true, maxAge: 3600 }))
627
679
  ```
628
680
 
629
- | Option | Type | Default | Description |
630
- |--------|------|---------|-------------|
631
- | `origin` | `string\|string[]\|function` | `'*'` | Allowed origins |
632
- | `methods` | `string[]` | `['GET','POST','PUT','DELETE','PATCH','HEAD','OPTIONS']` | Allowed methods |
633
- | `allowedHeaders` | `string[]` | — | Custom allowed headers |
634
- | `exposedHeaders` | `string[]` | — | Response headers exposed to client |
635
- | `credentials` | `boolean` | `false` | Allow cookies/credentials |
636
- | `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) |
637
689
 
638
690
  ### flash [α] [UX]
639
691
 
@@ -643,7 +695,7 @@ Cookie-based flash message. Read from request, write via redirect.
643
695
  app.use(flash())
644
696
 
645
697
  app.get('/', (req, ctx) => {
646
- const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
698
+ const msg = ctx.flash.value // { type: 'success', text: 'Saved!' } or undefined
647
699
  })
648
700
 
649
701
  app.post('/save', (req, ctx) => {
@@ -651,8 +703,8 @@ app.post('/save', (req, ctx) => {
651
703
  })
652
704
  ```
653
705
 
654
- | Option | Type | Default | Description |
655
- |--------|------|---------|-------------|
706
+ | Option | Type | Default | Description |
707
+ | ------ | -------- | --------- | ----------- |
656
708
  | `name` | `string` | `'flash'` | Cookie name |
657
709
 
658
710
  ### cache [α] [DevTools]
@@ -660,30 +712,32 @@ app.post('/save', (req, ctx) => {
660
712
  Response caching middleware with memory and Redis stores. Caches GET/HEAD responses, with tag-based invalidation.
661
713
 
662
714
  ```ts
663
- app.use(cache()) // in-memory, 5min TTL
664
- app.use(cache({ ttl: 60_000, store: 'redis', redis: ctx.redis })) // Redis store
665
- app.use(cache({
666
- ttl: 30_000,
667
- tag: (req, ctx) => ctx.user ? `user:${ctx.user.id}` : undefined, // per-user invalidation
668
- }))
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
+ )
669
723
 
670
724
  // Programmatic invalidation
671
725
  const c = cache({ store: 'redis', redis: ctx.redis })
672
726
  app.use(c)
673
- await c.invalidate('users') // invalidate all entries tagged with 'users'
674
- await c.flush() // clear entire cache
727
+ await c.invalidate('users') // invalidate all entries tagged with 'users'
728
+ await c.flush() // clear entire cache
675
729
  ```
676
730
 
677
- | Option | Type | Default | Description |
678
- |--------|------|---------|-------------|
679
- | `ttl` | `number` | `300000` (5min) | Cache TTL in ms |
680
- | `store` | `'memory' \| 'redis' \| CacheStore` | `'memory'` | Cache store backend |
681
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
682
- | `key` | `(req) => string` | SHA256(method+URL) | Custom cache key |
683
- | `tag` | `(req, ctx) => string \| string[]` | — | Tag for grouped invalidation |
684
- | `cacheCookies` | `boolean` | `false` | Cache responses with Set-Cookie |
685
- | `cacheStatus` | `number[]` | `[200]` | Status codes to cache |
686
- | `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 |
687
741
 
688
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.
689
743
 
@@ -691,7 +745,11 @@ Cached responses include `X-Cache: HIT` and `Age` headers. Requests with `Author
691
745
  import { MemoryCache, RedisCache } from 'weifuwu'
692
746
 
693
747
  const mem = new MemoryCache()
694
- 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
+ )
695
753
  mem.close()
696
754
  ```
697
755
 
@@ -699,17 +757,17 @@ mem.close()
699
757
 
700
758
  ```ts
701
759
  app.use(csrf())
702
- // ctx.csrfToken — set on GET/HEAD/OPTIONS
760
+ // ctx.csrf.token — set on GET/HEAD/OPTIONS
703
761
  // Auto-validates x-csrf-token or x-xsrf-token header on POST/PUT/DELETE/PATCH
704
762
  // Falls back to body field matching the key name
705
763
  ```
706
764
 
707
- | Option | Default | Description |
708
- |--------|---------|-------------|
709
- | `cookie` | `'_csrf'` | Cookie name |
710
- | `header` | `'x-csrf-token'` | Header name (also accepts `x-xsrf-token`) |
711
- | `key` | `'_csrf'` | Body field fallback |
712
- | `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 |
713
771
 
714
772
  ### deploy [β] [Networking]
715
773
 
@@ -719,26 +777,30 @@ Multi-process manager with reverse proxy, health checks, auto-restart, and zero-
719
777
  import { deploy, defineConfig } from 'weifuwu'
720
778
 
721
779
  // Local
722
- await deploy(defineConfig({
723
- apps: { blog: {}, api: {} },
724
- }))
780
+ await deploy(
781
+ defineConfig({
782
+ apps: { blog: {}, api: {} },
783
+ }),
784
+ )
725
785
 
726
786
  // Production
727
- await deploy(defineConfig({
728
- domain: 'example.com',
729
- deployToken: process.env.DEPLOY_TOKEN,
730
- apps: { blog: {}, api: {} },
731
- }))
787
+ await deploy(
788
+ defineConfig({
789
+ domain: 'example.com',
790
+ deployToken: process.env.DEPLOY_TOKEN,
791
+ apps: { blog: {}, api: {} },
792
+ }),
793
+ )
732
794
  ```
733
795
 
734
796
  **Auto-derived defaults** — each app key derives `dir`, `port`, `entry`, and `path`:
735
797
 
736
- | Field | Default | Rule |
737
- |-------|---------|------|
738
- | `dir` | App key | `blog` → `'./blog'` |
739
- | `entry` | `'index.ts'` | Default entry file |
740
- | `port` | `3001+` | Auto-incremented from 3001 |
741
- | `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 |
742
804
 
743
805
  Override any field explicitly:
744
806
 
@@ -762,7 +824,11 @@ apps: {
762
824
  **Blue-green** — zero-downtime via `ports`:
763
825
 
764
826
  ```ts
765
- apps: { blog: { ports: [3001, 3002] } }
827
+ apps: {
828
+ blog: {
829
+ ports: [3001, 3002]
830
+ }
831
+ }
766
832
  ```
767
833
 
768
834
  **WebSocket** — automatically bridged through the gateway.
@@ -771,15 +837,15 @@ apps: { blog: { ports: [3001, 3002] } }
771
837
 
772
838
  **Management API** — all endpoints require `Authorization: Bearer <deployToken>`:
773
839
 
774
- | Endpoint | Method | Description |
775
- |----------|--------|-------------|
776
- | `/_deploy/apps` | GET | List apps |
777
- | `/_deploy/apps/:name` | GET | App details |
778
- | `/_deploy/apps/:name/deploy` | POST | Restart |
779
- | `/_deploy/apps/:name/restart` | POST | Restart |
780
- | `/_deploy/apps/:name/stop` | POST | Stop |
781
- | `/_deploy/apps/:name/start` | POST | Start |
782
- | `/_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 |
783
849
 
784
850
  ```bash
785
851
  curl -H "Authorization: Bearer my-token" http://localhost:3000/_deploy/apps
@@ -796,28 +862,61 @@ Restart=always
796
862
 
797
863
  **DeployConfig:**
798
864
 
799
- | Option | Default | Description |
800
- |--------|---------|-------------|
801
- | `domain` | `'localhost'` | Root domain |
802
- | `port` | `3000` | Gateway port |
803
- | `deployToken` | — | Bearer token for management API |
804
- | `defaultApp` | — | Fallback route |
805
- | `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>` |
806
872
 
807
873
  **AppConfig:**
808
874
 
809
- | Field | Default | Description |
810
- |-------|---------|-------------|
811
- | `dir` | App key | Directory containing the app |
812
- | `port` | Auto (3001+) | Internal port |
813
- | `entry` | `'index.ts'` | Entry file |
814
- | `path` | `'/key'` (local) | URL path prefix |
815
- | `env` | — | Environment variables |
816
- | `healthEndpoint` | `/` | Health check path |
817
- | `buildCommand` | — | Build command |
818
- | `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 |
819
885
 
886
+ ### env [α] [DevTools]
820
887
 
888
+ Environment variable middleware. Injects `ctx.env` with all `WEIFUWU_PUBLIC_*` variables (prefix stripped).
889
+ Safe to expose to the client.
890
+
891
+ ```ts
892
+ import { env, loadEnv } from 'weifuwu'
893
+ loadEnv() // Load .env into process.env
894
+ app.use(env()) // → ctx.env
895
+
896
+ app.get('/config', (req, ctx) => {
897
+ return Response.json({ apiUrl: ctx.env.API_URL })
898
+ })
899
+ ```
900
+
901
+ Helper utilities:
902
+
903
+ ```ts
904
+ import { isDev, isProd, isBundled, getPublicEnv } from 'weifuwu'
905
+
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
910
+ ```
911
+
912
+ | Function | Description |
913
+ | ---------------- | ---------------------------------------------------------------- |
914
+ | `loadEnv(path?)` | Load `.env` file into `process.env` (does not override existing) |
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 |
821
920
 
822
921
  ### graphql [β] [API]
823
922
 
@@ -825,22 +924,22 @@ Restart=always
825
924
  const handler: GraphQLHandler = () => ({
826
925
  schema: `type Query { hello: String }`,
827
926
  resolvers: { Query: { hello: () => 'world' } },
828
- graphiql: true, // GET / returns GraphiQL IDE
829
- maxDepth: 10, // max query nesting (default 10, 0 = disable)
830
- 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
831
930
  })
832
931
  app.use('/graphql', graphql(handler))
833
932
  ```
834
933
 
835
- | Option | Type | Default | Description |
836
- |--------|------|---------|-------------|
837
- | `schema` | `string \| GraphQLSchema` | — | SDL string or pre-built schema |
838
- | `resolvers` | `object` | — | Resolver map |
839
- | `rootValue` | `any` | — | Root value for queries |
840
- | `context` | `(req, ctx) => object` | — | Per-request context factory |
841
- | `graphiql` | `boolean` | `false` | Serve GraphiQL IDE at GET / |
842
- | `maxDepth` | `number` | `10` | Max query nesting depth |
843
- | `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) |
844
943
 
845
944
  ### health [β] [API]
846
945
 
@@ -849,10 +948,10 @@ app.use('/health', health())
849
948
  // Returns 200 on success, 503 when check throws
850
949
  ```
851
950
 
852
- | Option | Type | Default | Description |
853
- |--------|------|---------|-------------|
854
- | `path` | `string` | `'/health'` | Health check endpoint |
855
- | `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 |
856
955
 
857
956
  ### helmet [α] [Security]
858
957
 
@@ -863,17 +962,17 @@ app.use(helmet())
863
962
  app.use(helmet({ contentSecurityPolicy: "default-src 'self'", xFrameOptions: 'DENY' }))
864
963
  ```
865
964
 
866
- | Option | Default | Description |
867
- |--------|---------|-------------|
868
- | `contentSecurityPolicy` | `"default-src 'self'"` | CSP policy |
869
- | `xFrameOptions` | `'SAMEORIGIN'` | Frame-embedding policy |
870
- | `strictTransportSecurity` | `'max-age=15552000; includeSubDomains'` | HSTS |
871
- | `referrerPolicy` | `'no-referrer'` | Referrer header |
872
- | `xContentTypeOptions` | `'nosniff'` | MIME sniffing protection |
873
- | `permissionsPolicy` | — | Feature permissions policy |
874
- | `crossOriginEmbedderPolicy` | — | COEP header |
875
- | `crossOriginOpenerPolicy` | — | COOP header |
876
- | `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 |
877
976
 
878
977
  ### iii [β] — Worker / Function / Trigger [API]
879
978
 
@@ -886,30 +985,30 @@ app.use('/iii', engine)
886
985
  app.ws('/iii', engine.wsHandler())
887
986
 
888
987
  const w = createWorker('orders')
889
- 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
+ )
890
991
  engine.addWorker(w)
891
992
  await engine.trigger({ function_id: 'orders::create', payload: { items: ['apple'] } })
892
993
  ```
893
994
 
894
- | Option | Type | Default | Description |
895
- |--------|------|---------|-------------|
896
- | `pg` | `object` | — | PostgreSQL client for persistent triggers |
897
- | `redis` | `object` | — | Redis client for streams |
898
- | `streamTTL` | `number` | `3600` | Redis stream key TTL (seconds, 0 = no expiry) |
899
-
900
- | Method | Description |
901
- |--------|-------------|
902
- | `.addWorker(w)` | Register a worker |
903
- | `.removeWorker(w)` | Remove a worker |
904
- | `.trigger({ function_id, payload, action?, timeout_ms? })` | Invoke a function |
905
- | `.listWorkers()` | List registered workers |
906
- | `.listFunctions()` | List registered functions |
907
- | `.listTriggers()` | List registered triggers |
908
- | `.wsHandler()` | WebSocket handler |
909
- | `.migrate()` | DB setup |
910
- | `.shutdown()` | Clean shutdown |
911
-
912
-
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 |
913
1012
 
914
1013
  ### knowledgeBase [β] — RAG with pgvector [AI]
915
1014
 
@@ -950,21 +1049,20 @@ app.get('/search', async (req, ctx) => {
950
1049
  })
951
1050
  ```
952
1051
 
953
- | Option | Type | Default | Description |
954
- |--------|------|---------|-------------|
955
- | `pg` | `PostgresClient` | — | **Required.** PostgreSQL client |
956
- | `provider` | `AIProvider` | — | **Required.** AI provider for embedding |
957
- | `table` | `string` | `'_kb_docs'` | Database table name |
958
- | `chunkSize` | `number` | `512` | Max characters per chunk |
959
- | `chunkOverlap` | `number` | `64` | Overlap between chunks |
960
- | `searchLimit` | `number` | `5` | Default search result count |
961
- | `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) |
962
1061
 
963
1062
  Documents are split on paragraph boundaries (`\n\n`). Re-ingesting the same key
964
1063
  replaces old chunks. Provider's `embed()` is used automatically.
965
1064
  The HNSW index enables fast approximate nearest-neighbor search (cosine distance).
966
1065
 
967
-
968
1066
  ### logdb [β] [API]
969
1067
 
970
1068
  PostgreSQL structured event logging with monthly partitioning.
@@ -973,69 +1071,79 @@ PostgreSQL structured event logging with monthly partitioning.
973
1071
  const logger = logdb({ pg })
974
1072
  await logger.migrate()
975
1073
  app.use('/logs', logger)
976
- await logger.clean(12) // drop partitions older than 12 months
1074
+ await logger.clean(12) // drop partitions older than 12 months
977
1075
  await logger.log({ level: 'info', source: 'app', message: 'hello', metadata: { userId: 1 } })
978
1076
  ```
979
1077
 
980
- | Option | Type | Default | Description |
981
- |--------|------|---------|-------------|
982
- | `pg` | `object` | — | PostgreSQL client |
983
- | `table` | `string` | `'_log_entries'` | Table name |
1078
+ | Option | Type | Default | Description |
1079
+ | ------- | -------- | ---------------- | ----------------- |
1080
+ | `pg` | `object` | — | PostgreSQL client |
1081
+ | `table` | `string` | `'_log_entries'` | Table name |
984
1082
 
985
- | Method | Path | Description |
986
- |--------|------|-------------|
987
- | POST | `/` | Create log entry |
988
- | GET | `/` | Query (`?level=`, `?source=`, `?after=`, `?before=`, `?meta.*=`) |
989
- | 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 |
990
1088
 
991
1089
  ### logger [α] [DevTools]
992
1090
 
993
1091
  ```ts
994
- app.use(logger()) // GET /hello 200 5ms
995
- 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
996
1094
  ```
997
1095
 
998
- | Option | Type | Default | Description |
999
- |--------|------|---------|-------------|
1000
- | `format` | `'short' \| 'combined'` | `'short'` | Log format: path only, or path + query params |
1096
+ | Option | Type | Default | Description |
1097
+ | -------- | --------------------------------- | --------- | ------------------------------------------------------------- |
1098
+ | `format` | `'short' \| 'combined' \| 'json'` | `'short'` | Log format: path only, path + query params, or JSON to stderr |
1001
1099
 
1002
1100
  ### mailer [γ] [Networking]
1003
1101
 
1004
1102
  ```ts
1005
- const mail = mailer({ from: 'noreply@example.com', transport: 'smtp://user:pass@smtp.example.com:587' })
1006
- 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
+ })
1007
1114
  ```
1008
1115
 
1009
- | Option | Type | Default | Description |
1010
- |--------|------|---------|-------------|
1011
- | `transport` | `string\|object` | — | Nodemailer transport config or connection string |
1012
- | `from` | `string` | — | Default sender address |
1013
- | `send` | `function` | — | Custom send function (alternative to transport) |
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) |
1014
1121
 
1122
+ ### oauthLogin (via user()) — Social login (OAuth 2.0 client) [Security]
1015
1123
 
1016
-
1017
- ### oauthClient [β] — Social login (OAuth 2.0 client) [Security]
1124
+ Social login is built into the [`user()`](#user-β) module via the `oauthLogin` option — no separate import needed.
1018
1125
 
1019
1126
  ```ts
1020
- import { oauthClient } from 'weifuwu'
1021
-
1022
- app.use(session()) // required — stores OAuth state
1023
- app.use(user({ pg, jwtSecret })) // required — user management
1024
- app.use('/auth', oauthClient({ // mounts /auth/google, /auth/google/callback
1127
+ app.use(session()) // required stores OAuth state
1128
+ const u = user({
1025
1129
  pg,
1026
- jwtSecret,
1027
- redirectUrl: '/dashboard',
1028
- providers: {
1029
- google: {
1030
- clientId: process.env.GOOGLE_CLIENT_ID,
1031
- clientSecret: process.env.GOOGLE_CLIENT_SECRET,
1032
- },
1033
- github: {
1034
- clientId: process.env.GITHUB_CLIENT_ID,
1035
- clientSecret: process.env.GITHUB_CLIENT_SECRET,
1130
+ jwtSecret: process.env.JWT_SECRET!,
1131
+ oauthLogin: {
1132
+ redirectUrl: '/dashboard',
1133
+ providers: {
1134
+ google: {
1135
+ clientId: process.env.GOOGLE_CLIENT_ID,
1136
+ clientSecret: process.env.GOOGLE_CLIENT_SECRET,
1137
+ },
1138
+ github: {
1139
+ clientId: process.env.GITHUB_CLIENT_ID,
1140
+ clientSecret: process.env.GITHUB_CLIENT_SECRET,
1141
+ },
1036
1142
  },
1037
1143
  },
1038
- }))
1144
+ })
1145
+ await u.migrate()
1146
+ app.use(u) // POST /register, POST /login, GET /auth/:provider, GET /auth/:provider/callback
1039
1147
  ```
1040
1148
 
1041
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).
@@ -1043,40 +1151,37 @@ app.use('/auth', oauthClient({ // mounts /auth/google, /auth/google/ca
1043
1151
  Supports custom providers via `authUrl`, `tokenUrl`, `userUrl`, and `parseUser`:
1044
1152
 
1045
1153
  ```ts
1046
- app.use('/auth', oauthClient({
1154
+ const u = user({
1047
1155
  pg,
1048
- jwtSecret,
1049
- providers: {
1050
- discord: {
1051
- clientId: process.env.DISCORD_CLIENT_ID,
1052
- clientSecret: process.env.DISCORD_CLIENT_SECRET,
1053
- authUrl: 'https://discord.com/api/oauth2/authorize',
1054
- tokenUrl: 'https://discord.com/api/oauth2/token',
1055
- userUrl: 'https://discord.com/api/users/@me',
1056
- parseUser: (data) => ({
1057
- id: data.id,
1058
- email: data.email ?? '',
1059
- name: data.global_name ?? data.username,
1060
- avatarUrl: data.avatar
1061
- ? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
1062
- : '',
1063
- }),
1156
+ jwtSecret: process.env.JWT_SECRET!,
1157
+ oauthLogin: {
1158
+ providers: {
1159
+ discord: {
1160
+ clientId: process.env.DISCORD_CLIENT_ID,
1161
+ clientSecret: process.env.DISCORD_CLIENT_SECRET,
1162
+ authUrl: 'https://discord.com/api/oauth2/authorize',
1163
+ tokenUrl: 'https://discord.com/api/oauth2/token',
1164
+ userUrl: 'https://discord.com/api/users/@me',
1165
+ parseUser: (data) => ({
1166
+ id: data.id,
1167
+ email: data.email ?? '',
1168
+ name: data.global_name ?? data.username,
1169
+ avatarUrl: data.avatar
1170
+ ? `https://cdn.discordapp.com/avatars/${data.id}/${data.avatar}.png`
1171
+ : '',
1172
+ }),
1173
+ },
1064
1174
  },
1065
1175
  },
1066
- }))
1176
+ })
1067
1177
  ```
1068
1178
 
1069
- | Option | Type | Default | Description |
1070
- |--------|------|---------|-------------|
1071
- | `pg` | `PostgresClient` | — | **Required.** Database connection |
1072
- | `jwtSecret` | `string` | — | **Required.** Must match `user()` module's secret |
1073
- | `providers` | `Record<string, OAuthProviderConfig>` | — | **Required.** Provider configs (Google/GitHub built-in, any custom) |
1074
- | `redirectUrl` | `string` | `'/'` | Post-login redirect destination |
1075
- | `expiresIn` | `string \| number` | `'24h'` | JWT expiry |
1076
- | `table` | `string` | `'_auth_providers'` | Provider-user link table name |
1077
-
1078
- The module auto-creates a `_auth_providers` table (`user_id`, `provider`, `provider_id`, `email`, `name`, `avatar_url`) on first request. Built-in providers (Google, GitHub) have preset URLs — you only need to provide `clientId` and `clientSecret`.
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 |
1079
1183
 
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.
1080
1185
 
1081
1186
  ### messager [β] [Networking]
1082
1187
 
@@ -1090,20 +1195,18 @@ app.ws('/ws', msg.wsHandler())
1090
1195
  await msg.send(channelId, 'System message', { sender_type: 'system', sender_id: 'bot' })
1091
1196
  ```
1092
1197
 
1093
- | Option | Type | Default | Description |
1094
- |--------|------|---------|-------------|
1095
- | `pg` | `object` | — | PostgreSQL client |
1096
- | `agents` | `AgentModule` | — | Agent module for routing |
1097
- | `webhookTimeout` | `number` | — | Webhook timeout |
1098
- | `redis` | `object` | — | Redis client |
1099
-
1100
- | Method | Description |
1101
- |--------|-------------|
1102
- | `.wsHandler()` | WebSocket handler (channels, typing, read receipts) |
1103
- | `.send(channel, content, opts?)` | Send message to channel |
1104
- | `.close()` | Cleanup |
1105
-
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 |
1106
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 |
1107
1210
 
1108
1211
  ### opencode [β] [AI]
1109
1212
 
@@ -1121,35 +1224,35 @@ app.use('/opencode', oc)
1121
1224
  app.ws('/opencode', oc.wsHandler())
1122
1225
  ```
1123
1226
 
1124
- | Option | Type | Default | Description |
1125
- |--------|------|---------|-------------|
1126
- | `pg` | `object` | — | PostgreSQL client |
1127
- | `model` | `string` | — | AI model name (e.g. `'gpt-4o'`, `'deepseek-v4-flash'`) |
1128
- | `baseURL` | `string` | — | OpenAI-compatible API base URL |
1129
- | `apiKey` | `string` | — | API key for the model |
1130
- | `workspace` | `string` | — | Project directory |
1131
- | `systemPrompt` | `string` | — | Custom system prompt |
1132
- | `skills` | `object[]` | — | Custom skill definitions |
1133
- | `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 |
1134
1237
 
1135
1238
  ### postgres [α] [Database]
1136
1239
 
1137
1240
  Type-safe PostgreSQL client with schema builder, CRUD, migrations, soft delete, and JSONB/vector support.
1138
1241
 
1139
1242
  ```ts
1140
- const pg = postgres() // reads DATABASE_URL
1141
- app.use(pg) // injects ctx.sql
1243
+ const pg = postgres() // reads DATABASE_URL
1244
+ app.use(pg) // injects ctx.sql
1142
1245
  ```
1143
1246
 
1144
- | Option | Type | Default | Description |
1145
- |--------|------|---------|-------------|
1146
- | `connection` | `string` | `DATABASE_URL` env | PostgreSQL connection string |
1147
- | `max` | `number` | `10` | Max pool connections |
1148
- | `ssl` | `boolean\|object` | — | SSL options |
1149
- | `idle_timeout` | `number` | `30` | Idle timeout (seconds) |
1150
- | `connect_timeout` | `number` | `30` | Connection timeout |
1151
- | `statementTimeout` | `number` | `30_000` | Per-statement timeout (ms, 0 = disable) |
1152
- | `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 |
1153
1256
 
1154
1257
  ```ts
1155
1258
  // Raw SQL via tagged template
@@ -1165,40 +1268,46 @@ const users = pg.table('_users', {
1165
1268
  active: boolean('active').default(true),
1166
1269
  ...timestamps(),
1167
1270
  })
1168
- await users.create() // DDL — no need to pass sql
1271
+ await users.create() // DDL — no need to pass sql
1169
1272
  await users.createIndex('email')
1170
1273
 
1171
1274
  // CRUD — sql already bound
1172
1275
  await users.insert({ name: 'Alice' })
1173
- 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
+ )
1174
1280
  await users.upsert({ email: 'alice@test.com' }, 'email')
1175
1281
 
1176
1282
  // Reuse schema without redefining fields
1177
1283
  import { pgTable } from 'weifuwu'
1178
- const usersSchema = pgTable('_users', { id: serial('id'), name: text('name') }) // define once
1179
- 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
1180
1286
 
1181
1287
  // Transactions — with auto-retry on deadlock/serialization failure
1182
- await pg.transaction(async (sql) => {
1183
- const txUsers = users.withSql(sql)
1184
- return txUsers.insert({ name: 'Bob' })
1185
- }, { 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
+ )
1186
1295
 
1187
1296
  // Soft delete — automatic if deleted_at column exists
1188
- await users.delete(1) // SET deleted_at = NOW()
1189
- await users.hardDelete(1) // DELETE FROM
1190
- 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)
1191
1300
 
1192
1301
  // JSONB queries
1193
1302
  const logs = pg.table('logs', { meta: jsonb<{ service: string }>('meta') })
1194
1303
  await logs.readMany(contains('meta', { service: 'auth' }))
1195
1304
 
1196
1305
  // Connection pool visibility
1197
- 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 }
1198
1307
 
1199
1308
  // Migration tracking
1200
- await pg.migrate() // creates _weifuwu_migrations
1201
- await pg.markMigrated('myModule') // idempotent
1309
+ await pg.migrate() // creates _weifuwu_migrations
1310
+ await pg.markMigrated('myModule') // idempotent
1202
1311
  const done = await pg.isMigrated('myModule')
1203
1312
 
1204
1313
  // Partitioned tables
@@ -1212,61 +1321,65 @@ await logs.create({ partitionBy: partitionBy('range', 'created_at') })
1212
1321
  | `pg.table(schema)` | Reusing a schema without duplicating field definitions |
1213
1322
  | `pgTable('t', cols)` | No `pg` reference (utility modules, standalone schema files) |
1214
1323
 
1215
- | Column builder | Type | Notes |
1216
- |---------------|------|-------|
1217
- | `serial(name)` | `number` | Auto-increment |
1218
- | `uuid(name)` | `string` | — |
1219
- | `text(name)` | `string` | — |
1220
- | `integer(name)` | `number` | — |
1221
- | `boolean(name)` / `boolean_(name)` | `boolean` | `_` suffix for JS reserved word |
1222
- | `timestamptz(name)` | `string` | — |
1223
- | `jsonb<T>(name)` | `T` | Generic for typed JSONB access |
1224
- | `textArray(name)` | `string[]` | TEXT[] |
1225
- | `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 |
1226
1335
 
1227
1336
  **Column modifiers:** `.primaryKey()`, `.notNull()`, `.nullable()`, `.default(val)`, `.unique()`, `.references(table, column?, onDelete?)`.
1228
1337
 
1229
1338
  **CRUD methods:**
1230
1339
 
1231
- | Method | Description |
1232
- |--------|-------------|
1233
- | `insert(data)` | INSERT + RETURNING \*, returns the inserted row |
1234
- | `insertMany(data)` | Bulk INSERT + RETURNING \*, returns rows |
1235
- | `read(id, opts?)` | SELECT by detected primary key + auto soft-delete filter |
1236
- | `readMany(where?, opts?)` | Filtered query with `{ count, data }` — auto-filters soft-deleted |
1237
- | `update(id, data)` | UPDATE by primary key + RETURNING \*, returns updated row |
1238
- | `updateMany(where, data)` | Bulk UPDATE, returns affected row count |
1239
- | `delete(id)` | Soft delete if `deleted_at` exists, else hard delete |
1240
- | `hardDelete(id)` | Always DELETE FROM |
1241
- | `deleteMany(where)` | Soft bulk delete if `deleted_at` exists |
1242
- | `hardDeleteMany(where)` | Always DELETE FROM |
1243
- | `upsert(data, conflict)` | INSERT ON CONFLICT DO UPDATE, returns row |
1244
- | `count(where?)` | SELECT COUNT(\*) — auto-filters soft-deleted |
1245
- | `create(opts?)` | CREATE TABLE IF NOT EXISTS |
1246
- | `drop(opts?)` | DROP TABLE IF EXISTS |
1247
- | `createIndex(columns, opts?)` | CREATE INDEX |
1248
- | `createUniqueIndex(columns)` | CREATE UNIQUE INDEX |
1249
- | `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) |
1250
1359
 
1251
1360
  **Where helpers** — composable query conditions:
1252
1361
 
1253
- | Helper | SQL |
1254
- |--------|-----|
1255
- | `eq(col, val)` | `"col" = val` |
1256
- | `ne(col, val)` | `"col" != val` |
1257
- | `gt` / `gte` / `lt` / `lte` | Comparison operators |
1258
- | `isNull(col)` / `isNotNull(col)` | `IS NULL` / `IS NOT NULL` |
1259
- | `like(col, pattern)` | `LIKE` |
1260
- | `contains(col, val)` | `@>` JSONB containment |
1261
- | `in_(col, vals)` | `= ANY(...)` |
1262
- | `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 |
1263
1372
 
1264
1373
  **PgModule** — base class for modules that need DB access:
1265
1374
 
1266
1375
  ```ts
1267
1376
  class MyModule extends PgModule {
1268
- async migrate() { /* run DDL */ }
1269
- async getUsers() { return this.table('users', {}).readMany() }
1377
+ async migrate() {
1378
+ /* run DDL */
1379
+ }
1380
+ async getUsers() {
1381
+ return this.table('users', {}).readMany()
1382
+ }
1270
1383
  }
1271
1384
  ```
1272
1385
 
@@ -1284,10 +1397,10 @@ const next = cronNext('0 9 * * 1-5')
1284
1397
  console.log(new Date(next))
1285
1398
  ```
1286
1399
 
1287
- | Function | Description |
1288
- |----------|-------------|
1289
- | `parsePattern(pattern)` | Parse 5-field cron pattern into `Set<number>[]` |
1290
- | `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 |
1291
1404
  | `cronNext(expr, from?)` | Calculate next matching timestamp (`from` defaults to now) |
1292
1405
 
1293
1406
  ### fts — Full-Text Search (PostgreSQL) [Database]
@@ -1318,11 +1431,11 @@ const results = await fts.search(pg.sql, articles, 'node.js framework', {
1318
1431
  await fts.dropIndex(pg.sql, articles)
1319
1432
  ```
1320
1433
 
1321
- | Function | Description |
1322
- |----------|-------------|
1434
+ | Function | Description |
1435
+ | ---------------------------------------- | ------------------------------ |
1323
1436
  | `createIndex(sql, table, fields, opts?)` | Create GIN/GiST tsvector index |
1324
- | `search(sql, table, query, opts?)` | Search with ts_rank ordering |
1325
- | `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 |
1326
1439
 
1327
1440
  Search options: `fields`, `limit` (20), `offset` (0), `headline` (false), `language` ('english'), `minRank`.
1328
1441
 
@@ -1342,10 +1455,10 @@ app.use(theme({ default: 'dark' }))
1342
1455
  // app.use('/', t)
1343
1456
  ```
1344
1457
 
1345
- | Option | Type | Default | Description |
1346
- |--------|------|---------|-------------|
1347
- | `default` | `string` | `'system'` | Default theme |
1348
- | `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) |
1349
1462
 
1350
1463
  ```ts
1351
1464
  // Server-side switching
@@ -1373,15 +1486,14 @@ app.use(i18n({ default: 'zh', dir: './locales' }))
1373
1486
  // app.use(l.middleware())
1374
1487
  // app.use('/', l)
1375
1488
  ```
1376
- ```
1377
1489
 
1378
- | Option | Type | Default | Description |
1379
- |--------|------|---------|-------------|
1380
- | `default` | `string` | `'en'` | Default locale |
1381
- | `dir` | `string` | — | Directory with `{locale}.json` files |
1382
- | `messages` | `object` | — | Inline translations: `{ zh: { welcome: '欢迎' } }` |
1383
- | `cookie` | `string` | `'locale'` | Cookie name (empty to disable) |
1384
- | `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 |
1385
1497
 
1386
1498
  ```ts
1387
1499
  // Handler
@@ -1408,7 +1520,9 @@ const q = queue({ store: 'memory' })
1408
1520
  // const q = queue({ store: 'redis', redis })
1409
1521
 
1410
1522
  // Register cron job (uses the same backend for persistence)
1411
- q.cron('*/5 * * * *', async () => { await cleanCache() })
1523
+ q.cron('*/5 * * * *', async () => {
1524
+ await cleanCache()
1525
+ })
1412
1526
 
1413
1527
  // Or use process/add for full queue semantics
1414
1528
  q.process('send-email', async (job) => {
@@ -1427,55 +1541,55 @@ q.add('weekly-report', {}, { schedule: '0 9 * * 1' })
1427
1541
  q.run()
1428
1542
  ```
1429
1543
 
1430
- | Option | Type | Default | Description |
1431
- |--------|------|---------|-------------|
1432
- | `store` | `'memory' \| 'pg' \| 'redis'` | `'memory'` | Backend store |
1433
- | `redis` | `object` | — | Redis client (required when `store: 'redis'`) |
1434
- | `url` | `string` | — | Redis URL (alternative to client) |
1435
- | `pg` | `object` | — | PostgreSQL client (required when `store: 'pg'`) |
1436
- | `prefix` | `string` | `'queue'` | Key/table prefix |
1437
- | `pollInterval` | `number` | `200` | Poll interval (ms) |
1438
-
1439
- | Method | Description |
1440
- |--------|-------------|
1441
- | `.cron(pattern, handler)` | Register a cron job (uses process + add internally) |
1442
- | `.add(type, payload, opts?)` | Add job (opts: `delay`, `schedule`) |
1443
- | `.process(type, handler)` | Register job processor |
1444
- | `.run()` | Start processing |
1445
- | `.stop()` | Stop processing |
1446
- | `.jobs(limit?)` | List pending jobs |
1447
- | `.failedJobs(limit?)` | List failed jobs with error messages |
1448
- | `.retryFailed(jobId)` | Retry a specific failed job |
1449
- | `.retryAllFailed(type?)` | Retry all failed jobs (optionally by type) |
1450
- | `.dashboard()` | Returns a Router with management endpoints |
1451
- | `.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 |
1452
1566
 
1453
1567
  **Schedule (cron) field reference:**
1454
1568
 
1455
- | Field | Range |
1456
- |-------|-------|
1457
- | minute | 0–59 |
1458
- | hour | 0–23 |
1459
- | day of month | 1–31 |
1460
- | month | 1–12 |
1461
- | 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) |
1462
1576
 
1463
1577
  Supported cron syntax: `*` (any), `*/n` (every n), `n-m` (range), `n,m,o` (list), `n` (exact).
1464
1578
 
1465
1579
  **Dashboard endpoints** (mount via `app.use('/__queue', q.dashboard())`):
1466
1580
 
1467
- | Method | Path | Description |
1468
- |--------|------|-------------|
1469
- | GET | `/` | Queue stats + pending/failed counts by type |
1470
- | GET | `/:type/failed` | List failed jobs for a type |
1471
- | POST | `/:type/retry` | Retry all failed jobs of a type |
1472
- | 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 |
1473
1587
 
1474
1588
  ### rateLimit [α] [Security]
1475
1589
 
1476
1590
  ```ts
1477
- app.use(rateLimit({ max: 100, window: 60_000 })) // 100 req/min, in-memory
1478
- 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
1479
1593
  app.use(rateLimit({ key: (req) => req.headers.get('x-api-key') ?? 'anonymous' }))
1480
1594
 
1481
1595
  // Multi-process: Redis-backed rate limiting
@@ -1485,31 +1599,31 @@ app.use(rateLimit({ max: 100, store: 'redis', redis: ctx.redis }))
1485
1599
  // m.stop() — clear interval (memory) or Redis cleanup
1486
1600
  ```
1487
1601
 
1488
- | Option | Type | Default | Description |
1489
- |--------|------|---------|-------------|
1490
- | `max` | `number` | `100` | Max requests per window |
1491
- | `window` | `number` | `60_000` | Window duration (ms) |
1492
- | `key` | `(req) => string` | IP-based | Key function |
1493
- | `message` | `string` | `'Too Many Requests'` | 429 response body |
1494
- | `store` | `'memory' \| 'redis'` | `'memory'` | Backend store |
1495
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1496
- | `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 |
1497
1611
 
1498
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.
1499
1613
 
1500
1614
  ### redis [α] [Database]
1501
1615
 
1502
1616
  ```ts
1503
- const r = redis() // reads REDIS_URL
1504
- app.use(r) // injects ctx.redis
1617
+ const r = redis() // reads REDIS_URL
1618
+ app.use(r) // injects ctx.redis
1505
1619
  await ctx.redis.set('key', 'value')
1506
1620
  // r.close() — cleanup
1507
1621
  ```
1508
1622
 
1509
- | Option | Type | Default | Description |
1510
- |--------|------|---------|-------------|
1511
- | `url` | `string` | `REDIS_URL` env | Redis connection string |
1512
- | (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 |
1513
1627
 
1514
1628
  ### requestId [α] [DevTools]
1515
1629
 
@@ -1519,46 +1633,67 @@ app.use(requestId({ header: 'X-Request-Id', generator: () => crypto.randomUUID()
1519
1633
  // Sets X-Request-ID header on responses, available as ctx.requestId
1520
1634
  ```
1521
1635
 
1522
- | Option | Type | Default | Description |
1523
- |--------|------|---------|-------------|
1524
- | `header` | `string` | `'X-Request-ID'` | Header name to read/write |
1525
- | `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 |
1640
+
1641
+ ### trace [α] [DevTools]
1526
1642
 
1527
- ### trace [γ] [DevTools]
1643
+ Request-scoped tracing via `AsyncLocalStorage`. Use as middleware to inject `ctx.trace`:
1528
1644
 
1529
- Request-scoped tracing via `AsyncLocalStorage`. Used internally by `serve()`.
1645
+ ```ts
1646
+ import { trace } from 'weifuwu'
1647
+ app.use(trace()) // → ctx.trace
1648
+ app.use(trace({ header: 'X-Trace-Id' })) // custom header
1649
+
1650
+ app.get('/', (req, ctx) => {
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
1654
+ })
1655
+ ```
1656
+
1657
+ | Option | Type | Default | Description |
1658
+ | ----------- | -------------- | --------------------- | ---------------------- |
1659
+ | `header` | `string` | `'X-Request-ID'` | Request ID header name |
1660
+ | `generator` | `() => string` | `crypto.randomUUID()` | Custom ID generator |
1661
+
1662
+ Utility functions (also available standalone):
1530
1663
 
1531
1664
  ```ts
1532
- import { currentTraceId, runWithTrace, traceElapsed } from 'weifuwu'
1665
+ import { currentTraceId, runWithTrace, traceElapsed, currentTrace } from 'weifuwu'
1533
1666
 
1534
- // Inside a middleware or handler
1535
1667
  const traceId = currentTraceId() // UUID or incoming X-Trace-Id
1536
1668
  const elapsed = traceElapsed() // ms since request started
1669
+ runWithTrace(incomingId, () => { ... }) // manual scope
1537
1670
  ```
1538
1671
 
1539
- | Function | Description |
1540
- |----------|-------------|
1541
- | `currentTraceId()` | Current request trace ID, or `undefined` outside a request |
1542
- | `currentTrace()` | Full `{ traceId, startTime }` context |
1543
- | `traceElapsed()` | Milliseconds elapsed since the trace started |
1544
- | `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 |
1545
1678
 
1546
1679
  ### s3 [α] — S3-compatible object storage [Networking]
1547
1680
 
1548
1681
  ```ts
1549
1682
  import { s3 } from 'weifuwu'
1550
1683
 
1551
- app.use(s3({
1552
- bucket: 'my-app',
1553
- region: 'us-east-1',
1554
- endpoint: process.env.S3_URL, // MinIO / R2 / AWS
1555
- forcePathStyle: true, // required for MinIO
1556
- credentials: {
1557
- accessKeyId: process.env.S3_ACCESS_KEY,
1558
- secretAccessKey: process.env.S3_SECRET_KEY,
1559
- },
1560
- publicUrl: 'https://cdn.example.com', // for unsigned public URLs
1561
- }))
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
+ )
1562
1697
  ```
1563
1698
 
1564
1699
  Injects `ctx.s3` with methods for S3-compatible object storage.
@@ -1587,14 +1722,14 @@ const publicUrl = await ctx.s3.url('images/logo.png', { expiresIn: 0 })
1587
1722
  const keys = await ctx.s3.list('images/')
1588
1723
  ```
1589
1724
 
1590
- | Option | Type | Default | Description |
1591
- |--------|------|---------|-------------|
1592
- | `bucket` | `string` | — | **Required.** S3 bucket name |
1593
- | `region` | `string` | `'us-east-1'` | AWS region |
1594
- | `endpoint` | `string` | — | Custom endpoint (MinIO, R2, B2) |
1595
- | `forcePathStyle` | `boolean` | `false` | Path-style addressing (required for MinIO) |
1596
- | `credentials` | `{ accessKeyId, secretAccessKey }` | — | Falls back to AWS env vars / IAM role |
1597
- | `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 })` |
1598
1733
 
1599
1734
  Credentials can be omitted to use AWS environment variables (`AWS_ACCESS_KEY_ID`,
1600
1735
  `AWS_SECRET_ACCESS_KEY`) or IAM roles (EC2, ECS, Lambda).
@@ -1621,32 +1756,42 @@ minio:
1621
1756
  command: server /data
1622
1757
  ```
1623
1758
 
1624
-
1625
1759
  ### seo [β] + seoMiddleware [α] [API]
1626
1760
 
1627
1761
  ```ts
1628
- 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
+ )
1629
1770
  // GET /robots.txt, GET /sitemap.xml
1630
1771
 
1631
- 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
+ )
1632
1777
  ```
1633
1778
 
1634
1779
  Also exports `seoTags(config)` for generating meta/og/twitter tags as an HTML string.
1635
1780
 
1636
- | Option | Type | Default | Description |
1637
- |--------|------|---------|-------------|
1638
- | `baseUrl` | `string` | — | Base URL for sitemap URLs |
1639
- | `robots` | `RobotsRule[]` | `[{ userAgent: '*', allow: '/' }]` | Robots.txt rules |
1640
- | `sitemap` | `SitemapConfig` | — | Sitemap configuration (urls, resolve, cacheTTL) |
1641
- | `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`) |
1642
1787
 
1643
1788
  ### session [α] [Security]
1644
1789
 
1645
1790
  Cookie-based server-side session management with memory and Redis stores.
1646
1791
 
1647
1792
  ```ts
1648
- app.use(session()) // in-memory store (default)
1649
- 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
1650
1795
  app.use(session({ store: 'redis', redis, ttl: 30 * 60_000, cookieName: 'sid' }))
1651
1796
 
1652
1797
  app.get('/login', async (req, ctx) => {
@@ -1657,7 +1802,7 @@ app.get('/login', async (req, ctx) => {
1657
1802
  })
1658
1803
 
1659
1804
  app.get('/logout', async (req, ctx) => {
1660
- ctx.session.destroy() // or ctx.session = null
1805
+ ctx.session.destroy() // or ctx.session = null
1661
1806
  return Response.json({ ok: true })
1662
1807
  })
1663
1808
 
@@ -1667,19 +1812,19 @@ app.get('/logout', async (req, ctx) => {
1667
1812
  // Session mutations are auto-detected on property set/delete
1668
1813
  ```
1669
1814
 
1670
- | Option | Type | Default | Description |
1671
- |--------|------|---------|-------------|
1672
- | `store` | `'memory' \| 'redis' \| SessionStore` | `'memory'` | Session store backend |
1673
- | `redis` | `Redis` | — | Redis client (required when `store: 'redis'`) |
1674
- | `ttl` | `number` | `86400000` (24h) | Session TTL in ms |
1675
- | `cookieName` | `string` | `'__session'` | Cookie name |
1676
- | `cookie.httpOnly` | `boolean` | `true` | Cookie httpOnly flag |
1677
- | `cookie.secure` | `boolean` | `auto` | Cookie Secure flag (true in production) |
1678
- | `cookie.sameSite` | `string` | `'lax'` | SameSite policy |
1679
- | `cookie.path` | `string` | `'/'` | Cookie path |
1680
- | `cookie.domain` | `string` | — | Cookie domain |
1681
- | `secret` | `string` | — | HMAC-SHA256 sign the session cookie (`uuid.signature`). Prevents tampering **strongly recommended in production** |
1682
- | `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 |
1683
1828
 
1684
1829
  When `secret` is set, the cookie value is signed with HMAC-SHA256:
1685
1830
  `uuid.base64url(hmac)`. Tampered cookies are rejected and treated as new
@@ -1694,7 +1839,7 @@ from the store. Rotation happens transparently on the next request after
1694
1839
  ```ts
1695
1840
  import { MemoryStore, RedisStore } from 'weifuwu'
1696
1841
 
1697
- const mem = new MemoryStore() // auto-cleanup every 60s
1842
+ const mem = new MemoryStore() // auto-cleanup every 60s
1698
1843
  await mem.set('sid', { userId: 1 }, 86400000)
1699
1844
  mem.close()
1700
1845
 
@@ -1733,14 +1878,14 @@ app.use('/', ssr({ dir: './ui' }))
1733
1878
  └── lib/ ← utilities (does not affect routing)
1734
1879
  ```
1735
1880
 
1736
- | Location | Route |
1737
- |----------|-------|
1738
- | `app/page.tsx` | `GET /` |
1739
- | `app/[param]/page.tsx` | `GET /:param` |
1740
- | `app/layout.tsx` | Root layout (wraps all pages in its subtree) |
1741
- | `app/not-found.tsx` | 404 fallback for that subtree |
1742
- | `app/error.tsx` | Error boundary for that subtree |
1743
- | `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`) |
1744
1889
 
1745
1890
  **How hydration works:**
1746
1891
 
@@ -1751,13 +1896,13 @@ app.use('/', ssr({ dir: './ui' }))
1751
1896
  - The vendor bundle (react + react-dom + weifuwu client libs) is compiled once and cached
1752
1897
  - Page components are pre-compiled to `/__ssr/{hash}.js` — no runtime esbuild after first request
1753
1898
  - **Dev:** `createRoot` + render; **Production:** `hydrateRoot` (reuses SSR DOM)
1754
- - Both the hydration script and the page component share the same store via `globalThis.__WEIFUWU_CTX_STORE`
1899
+ - The hydration script and page component share the same SSR context store data flows seamlessly from server to client
1755
1900
  - Tailwind CSS served at `/__wfw/style/{hash}.css` (cached, content-hashed)
1756
1901
  - Dev mode extras: HMR WebSocket, file watcher, hot component replacement
1757
1902
 
1758
1903
  ```ts
1759
1904
  // Multiple independent SSR directories
1760
- app.use('/', ssr({ dir: './www' }))
1905
+ app.use('/', ssr({ dir: './www' }))
1761
1906
  app.use('/admin', ssr({ dir: './admin' }))
1762
1907
 
1763
1908
  // API routes coexist normally
@@ -1773,31 +1918,35 @@ Multi-tenant BaaS with dynamic table API and GraphQL.
1773
1918
  ```ts
1774
1919
  const t = tenant({ pg, usersTable: '_users' })
1775
1920
  await t.migrate()
1776
- app.use('/api', t.middleware()) // → ctx.tenant
1777
- app.use('/api', t) // dynamic CRUD
1921
+ app.use('/api', t.middleware()) // → ctx.tenant
1922
+ app.use('/api', t) // dynamic CRUD
1778
1923
  app.use('/graphql', t.graphql()) // dynamic GraphQL
1779
1924
  ```
1780
1925
 
1781
- | Option | Type | Default | Description |
1782
- |--------|------|---------|-------------|
1783
- | `pg` | `object` | — | PostgreSQL client |
1784
- | `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 |
1785
1930
 
1786
1931
  ### upload [α] [DevTools]
1787
1932
 
1788
1933
  ```ts
1789
- app.post('/upload', upload({ dir: './uploads', maxFileSize: 10_485_760, allowedTypes: ['image/jpeg', 'image/png'] }), (req, ctx) => {
1790
- // ctx.parsed.files.avatar → { name, type, size, path } or { name, type, size, buffer } (when no dir)
1791
- // Multiple files with same field name array
1792
- // ctx.parsed.fields.title 'hello'
1793
- })
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
+ )
1794
1943
  ```
1795
1944
 
1796
- | Option | Type | Default | Description |
1797
- |--------|------|---------|-------------|
1798
- | `dir` | `string` | — | Write files to disk (omit for in-memory) |
1799
- | `maxFileSize` | `number` | — | Max bytes per file |
1800
- | `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 |
1801
1950
 
1802
1951
  ### user [β] [Security]
1803
1952
 
@@ -1807,7 +1956,8 @@ Authentication: register, login, JWT, OAuth2 服务端, 社会化登录.
1807
1956
  const u = user({
1808
1957
  pg,
1809
1958
  jwtSecret: process.env.JWT_SECRET!,
1810
- oauthLogin: { // 可选 — 社会化登录
1959
+ oauthLogin: {
1960
+ // 可选 — 社会化登录
1811
1961
  providers: {
1812
1962
  github: { clientId: '...', clientSecret: '...' },
1813
1963
  google: { clientId: '...', clientSecret: '...' },
@@ -1815,26 +1965,26 @@ const u = user({
1815
1965
  },
1816
1966
  })
1817
1967
  await u.migrate()
1818
- app.use(u) // POST /register, POST /login
1819
- app.use(u.middleware()) // ctx.user
1968
+ app.use(u) // POST /register, POST /login
1969
+ app.use(u.middleware()) // ctx.user
1820
1970
  // GET /auth/github, GET /auth/github/callback (如配置 oauthLogin)
1821
1971
  ```
1822
1972
 
1823
- | Option | Type | Default | Description |
1824
- |--------|------|---------|-------------|
1825
- | `pg` | `object` | — | PostgreSQL client |
1826
- | `jwtSecret` | `string` | — | JWT signing secret |
1827
- | `table` | `string` | `'_users'` | Users table name |
1828
- | `expiresIn` | `string` | `'24h'` | JWT expiration |
1829
- | `oauth2` | `object` | — | OAuth2 服务端 config (PKCE flow) |
1830
- | `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? }` |
1831
1981
 
1832
- | Method | Description |
1833
- |--------|-------------|
1834
- | `.register(data)` | Register a new user programmatically |
1835
- | `.login(data)` | Log in programmatically |
1836
- | `.verify(token)` | Verify JWT token |
1837
- | `.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` |
1838
1988
 
1839
1989
  ### permissions [α] — RBAC [Security]
1840
1990
 
@@ -1848,11 +1998,14 @@ await perm.migrate()
1848
1998
  await perm.assignRole(userId, 'admin')
1849
1999
  await perm.grantPermission('admin', 'posts:create')
1850
2000
  await perm.grantPermission('admin', 'posts:edit')
1851
- await perm.grantPermission('admin', '*') // wildcard — all permissions
2001
+ await perm.grantPermission('admin', '*') // wildcard — all permissions
1852
2002
 
1853
2003
  // Use as middleware
1854
- app.use((req, ctx, next) => { ctx.user = { id: userId }; return next(req, ctx) })
1855
- 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
1856
2009
 
1857
2010
  // Route guards
1858
2011
  app.get('/admin', perm.requireRole('admin'), adminHandler)
@@ -1867,26 +2020,26 @@ app.get('/posts/:id', async (req, ctx) => {
1867
2020
  })
1868
2021
  ```
1869
2022
 
1870
- | Option | Type | Default | Description |
1871
- |--------|------|---------|-------------|
1872
- | `pg` | `object` | — | PostgreSQL client |
1873
- | `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`) |
1874
2027
 
1875
- | Method | Description |
1876
- |--------|-------------|
1877
- | `.assignRole(userId, role)` | Assign role to user (creates role if missing) |
1878
- | `.removeRole(userId, role)` | Remove role from user |
1879
- | `.grantPermission(role, permission)` | Grant permission to role |
1880
- | `.revokePermission(role, permission)` | Revoke permission from role |
1881
- | `.getUserRoles(userId)` | List user's roles |
1882
- | `.getUserPermissions(userId)` | List user's permissions (union of all roles) |
1883
- | `.requireRole(...roles)` | Middleware — rejects if user lacks any of the roles |
1884
- | `.requirePermission(...perms)` | Middleware — rejects if user lacks any permission |
1885
- | `.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 |
1886
2039
 
1887
2040
  ### validate [α] [DevTools]
1888
2041
 
1889
- ```ts
2042
+ ````ts
1890
2043
  import { z } from 'zod'
1891
2044
  const CreateUser = z.object({ name: z.string().min(1), email: z.string().email() })
1892
2045
  app.post('/users', validate({ body: CreateUser, query: z.object({ ref: z.string().optional() }) }), (req, ctx) => {
@@ -1909,14 +2062,14 @@ app.post('/contact', validate(), (req, ctx) => {
1909
2062
 
1910
2063
  // Or validate with Zod
1911
2064
  app.post('/contact', validate({ body: z.object({ email: z.string().email() }) }), handler)
1912
- ```
2065
+ ````
1913
2066
 
1914
- | Option | Type | Default | Description |
1915
- |--------|------|---------|-------------|
1916
- | `body` | `ZodSchema` | — | Body validation schema (omit to skip) |
1917
- | `query` | `ZodSchema` | — | Query validation schema |
1918
- | `params` | `ZodSchema` | — | URL params validation schema |
1919
- | `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 |
1920
2073
 
1921
2074
  ### webhook [β] [API]
1922
2075
 
@@ -1946,28 +2099,30 @@ wh.on('*', (event) => {
1946
2099
  })
1947
2100
  ```
1948
2101
 
1949
- | Option | Type | Default | Description |
1950
- |--------|------|---------|-------------|
1951
- | `stripe` | `PlatformConfig` | — | Stripe webhook config with `secret` |
1952
- | `github` | `PlatformConfig` | — | GitHub webhook config |
1953
- | `slack` | `PlatformConfig` | — | Slack webhook config |
1954
- | `custom` | `CustomVerifierConfig[]` | — | Custom signature verifiers |
1955
- | `replayProtection` | `boolean` | `true` | Deduplicate by event ID |
1956
- | `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) |
1957
2110
 
1958
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.
1959
2112
 
1960
2113
  ### Client-side navigation [δ] [Client]
1961
2114
 
1962
2115
  ```tsx
1963
- import { Link, useNavigate, useNavigating } from 'weifuwu/react'
1964
-
1965
- <Link href="/about" prefetch>About</Link> // client-side nav + prefetch on hover/visible
1966
- const navigate = useNavigate() // programmatic: navigate('/contact')
1967
- const loading = useNavigating() // reactive loading state
2116
+ import { Link, navigate, useNavigate, useNavigating } from 'weifuwu/react'
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
1968
2123
  ```
1969
2124
 
1970
- `navigate()` fetches SSR, extracts `__weifuwu_root`, replaces in-place. Middleware runs on server each nav — data is always fresh.
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.
1971
2126
 
1972
2127
  **Preference URLs** (`/__lang/`, `/__theme/`) are intercepted by modular interceptors registered via `addInterceptor()` — no page reload needed. Importing `useLocale` or `useTheme` registers the interceptor automatically.
1973
2128
 
@@ -2021,11 +2176,11 @@ function LangSwitch() {
2021
2176
  }
2022
2177
  ```
2023
2178
 
2024
- | Return | Description |
2025
- |--------|-------------|
2026
- | `locale` | Current locale string (from `ctx.i18n.locale`) |
2179
+ | Return | Description |
2180
+ | ------------------- | ----------------------------------------------------- |
2181
+ | `locale` | Current locale string (from `ctx.i18n.locale`) |
2027
2182
  | `setLocale(locale)` | Switch locale (calls `navigate('/__lang/' + locale)`) |
2028
- | `t` | Translate a key using loaded locale messages |
2183
+ | `t` | Translate a key using loaded locale messages |
2029
2184
 
2030
2185
  ```tsx
2031
2186
  import { useTheme } from 'weifuwu/react'
@@ -2033,8 +2188,8 @@ function ThemeToggle() {
2033
2188
  const { theme, resolvedTheme, setTheme } = useTheme()
2034
2189
  return (
2035
2190
  <>
2036
- <span>Current: {resolvedTheme}</span> {/* 'dark' | 'light' — never 'system' */}
2037
- <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)}>
2038
2193
  <option value="light">☀ Light</option>
2039
2194
  <option value="dark">🌙 Dark</option>
2040
2195
  <option value="system">💻 System</option>
@@ -2044,11 +2199,11 @@ function ThemeToggle() {
2044
2199
  }
2045
2200
  ```
2046
2201
 
2047
- | Return | Description |
2048
- |--------|-------------|
2049
- | `theme` | Raw preference (`'light'` \| `'dark'` \| `'system'`) |
2050
- | `resolvedTheme` | Resolved value (`'light'` \| `'dark'`) — `'system'` → matchMedia |
2051
- | `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)`) |
2052
2207
 
2053
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.
2054
2209
 
@@ -2058,11 +2213,17 @@ function ThemeToggle() {
2058
2213
  import { useLoaderData } from 'weifuwu/react'
2059
2214
  function Page() {
2060
2215
  const data = useLoaderData<{ posts: Post[] }>()
2061
- 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
+ )
2062
2223
  }
2063
2224
  ```
2064
2225
 
2065
- On the server, data flows from middleware → `ctx` → `setCtx(ctxValue)` (serialized via JSON). On the client, the hydration script calls `setCtx(ctxData)` which populates the shared store (`globalThis.__WEIFUWU_CTX_STORE`). `useLoaderData()` reads from the snapshot via `useSyncExternalStore` — no SSR-specific code needed in your components.
2226
+ On the server, data flows from middleware → `ctx` → `setCtx(ctxValue)` (serialized via JSON). On the client, the hydration script calls `setCtx(ctxData)` which populates the shared context store. `useLoaderData()` reads from the snapshot via `useSyncExternalStore` — no SSR-specific code needed in your components.
2066
2227
 
2067
2228
  **`addInterceptor(fn)`** — Register a URL interceptor. Interceptors run before SPA navigation; if one returns `true`, `navigate()` skips the fetch-and-swap.
2068
2229
 
@@ -2084,7 +2245,7 @@ app.use(flash())
2084
2245
 
2085
2246
  // Read flash
2086
2247
  app.get('/', (req, ctx) => {
2087
- const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
2248
+ const msg = ctx.flash.value // { type: 'success', text: 'Saved!' }
2088
2249
  })
2089
2250
 
2090
2251
  // Set flash + redirect
@@ -2106,8 +2267,8 @@ function Toast() {
2106
2267
  }
2107
2268
  ```
2108
2269
 
2109
- | Option | Type | Default | Description |
2110
- |--------|------|---------|-------------|
2270
+ | Option | Type | Default | Description |
2271
+ | ------ | -------- | --------- | ----------- |
2111
2272
  | `name` | `string` | `'flash'` | Cookie name |
2112
2273
 
2113
2274
  ### Dev mode [δ] [Client]
@@ -2116,7 +2277,7 @@ Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically regi
2116
2277
 
2117
2278
  - Inline hydration script uses `createRoot` + render (replaces SSR DOM)
2118
2279
  - Vendor bundle served at `/__wfw/v/bundle?h=<hash>` — compiled from source, unminified
2119
- - Hot component replacement: file changes → WebSocket message → browser imports hot bundle → `__WFW_REFRESH(NewComponent)` — `useState` values preserved
2280
+ - Hot component replacement: file changes → WebSocket message → browser imports hot bundle → component refreshed in place — `useState` values preserved
2120
2281
  - Tailwind CSS hot-reloads without page refresh
2121
2282
  - Layout changes trigger a full page reload
2122
2283
 
@@ -2125,7 +2286,17 @@ Auto-detected when `NODE_ENV === 'development'`. `ssr({dir})` automatically regi
2125
2286
  ## AI
2126
2287
 
2127
2288
  ```ts
2128
- 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'
2129
2300
  import { runWorkflow } from 'weifuwu'
2130
2301
 
2131
2302
  const provider = aiProvider()
@@ -2148,23 +2319,23 @@ app.post('/ask', async (req, ctx) => {
2148
2319
  })
2149
2320
  ```
2150
2321
 
2151
- | Option | Type | Default | Description |
2152
- |--------|------|---------|-------------|
2153
- | `baseURL` | `string` | `OPENAI_BASE_URL` env or `http://localhost:11434/v1` | API base URL |
2154
- | `apiKey` | `string` | `OPENAI_API_KEY` env or `'ollama'` | API key |
2155
- | `model` | `string` | `OPENAI_MODEL` env or `'qwen3:0.6b'` | Chat model name |
2156
- | `embeddingModel` | `string` | `OPENAI_EMBEDDING_MODEL` env or `'qwen3-embedding:0.6b'` | Embedding model name |
2157
- | `embeddingDimension` | `number` | `EMBEDDING_DIMENSION` env or `1024` | Vector dimension |
2158
-
2159
- | Method | Description |
2160
- |--------|-------------|
2161
- | `.model(name?)` | Get `LanguageModel` instance |
2162
- | `.embeddingModel(name?)` | Get `EmbeddingModel` instance |
2163
- | `.embed(text)` | Embed single text → `Promise<number[]>` |
2164
- | `.embedMany(texts)` | Batch embed → `Promise<number[][]>` |
2165
- | `.generateText(params)` | Generate text (model auto-injected) |
2166
- | `.streamText(params)` | Stream text (model auto-injected) |
2167
- | `.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 |
2168
2339
 
2169
2340
  ### DAG Workflow [AI]
2170
2341
 
@@ -2178,12 +2349,12 @@ const wf = runWorkflow({ tools, provider })
2178
2349
  const wf = runWorkflow({ tools, model: openai('gpt-4o') })
2179
2350
  ```
2180
2351
 
2181
- | Option | Type | Default | Description |
2182
- |--------|------|---------|-------------|
2183
- | `tools` | `object` | — | Registered tool definitions |
2184
- | `provider` | `AIProvider` | — | AI provider (uses `provider.model()` for LLM-generated workflow) |
2185
- | `model` | `LanguageModel` | — | Explicit model (overrides provider) |
2186
- | `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 |
2187
2358
 
2188
2359
  ---
2189
2360
 
@@ -2191,7 +2362,10 @@ const wf = runWorkflow({ tools, model: openai('gpt-4o') })
2191
2362
 
2192
2363
  ```ts
2193
2364
  import { createSSEStream, formatSSE, formatSSEData } from 'weifuwu'
2194
- 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
+ }
2195
2369
  app.get('/stream', (req, ctx) => createSSEStream(events()))
2196
2370
  ```
2197
2371
 
@@ -2206,16 +2380,24 @@ Every public symbol can be imported from `'weifuwu'`:
2206
2380
  ```ts
2207
2381
  serve, createTestServer, Router, ssr,
2208
2382
  Context, Handler, Middleware, ErrorHandler, ServeOptions, Server,
2209
- loadEnv, testApp, TestApp, TestRequest, TestResponse,
2210
- currentTraceId, currentTrace, runWithTrace, traceElapsed, TraceContext,
2383
+ loadEnv, env, isDev, isProd, isBundled, getPublicEnv,
2384
+ currentTraceId, currentTrace, runWithTrace, traceElapsed, trace, TraceContext,
2385
+ testApp, TestApp, TestRequest, TestResponse,
2386
+ createTestDb, withTestDb,
2387
+ getCookies, setCookie, deleteCookie,
2388
+ createSSEStream, formatSSE, formatSSEData, SSEEvent,
2389
+ DEFAULT_MAX_BODY, MIGRATIONS_TABLE,
2211
2390
  ```
2212
2391
 
2213
- ### Middleware modules
2392
+ ### Middleware / DevTools
2214
2393
 
2215
2394
  ```ts
2216
- auth, cors, csrf, compress, helmet, logger, rateLimit, requestId, validate, upload,
2217
- theme, i18n, flash, permissions, serveStatic, session, MemoryStore, RedisStore, SessionStore,
2218
- cache, MemoryCache, RedisCache, CacheStore
2395
+ logger, cors, compress, helmet,
2396
+ rateLimit, requestId, validate, upload,
2397
+ csrf, session, MemoryStore, RedisStore, SessionStore,
2398
+ cache, MemoryCache, RedisCache, CacheStore,
2399
+ flash, permissions,
2400
+ serveStatic, s3,
2219
2401
  ```
2220
2402
 
2221
2403
  ### Database
@@ -2225,55 +2407,87 @@ postgres, PostgresOptions, PostgresClient,
2225
2407
  redis, RedisOptions, RedisClient,
2226
2408
  queue, QueueOptions, QueueJob, Queue,
2227
2409
  PostgresInjected, RedisInjected, QueueInjected,
2228
- // Schema helpers — importable alongside postgres:
2410
+ // Schema helpers:
2229
2411
  pgTable, SQL, sql,
2230
2412
  ColumnBuilder, serial, uuid, text, integer, boolean, boolean_, timestamptz, jsonb, textArray, vector,
2231
2413
  partitionBy, timestamps, toDDL, PartitionByDef,
2232
2414
  Table, BoundTable, IndexOptions, FindOptions, CreateOptions,
2233
- eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not
2415
+ eq, ne, gt, gte, lt, lte, isNull, isNotNull, like, contains, in_, and, or, not,
2416
+ fts,
2234
2417
  ```
2235
2418
 
2236
- ### Client-side (from `'weifuwu/react'`)
2419
+ ### Security / Auth
2237
2420
 
2238
2421
  ```ts
2239
- TsxContext, setCtx, useCtx, addCtxRebuilder, useLoaderData,
2240
- useWebsocket, useAction, useFetch, useQueryState, createStore,
2241
- Link, useNavigate, useNavigating, addInterceptor,
2242
- useLocale, useTheme, applyTheme, useFlashMessage,
2243
- useAgentStream,
2244
- Head
2422
+ auth,
2423
+ user, UserModule, UserData, UserOptions, UserInjected, OAuthProviderConfig, OAuth2Client,
2424
+ permissions, PermissionsModule, PermissionsOptions,
2425
+ csrf, CsrfOptions, CsrfInjected,
2426
+ helmet, HelmetOptions,
2427
+ session, SessionStore, SessionOptions, SessionData, SessionInjected,
2428
+ rateLimit, RateLimitOptions,
2245
2429
  ```
2246
- export type { UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState } from 'weifuwu/react'
2247
2430
 
2248
- ### AI Provider (framework abstraction)
2431
+ ### UX Middleware
2249
2432
 
2250
2433
  ```ts
2251
- aiProvider, AIProvider, AIProviderOptions
2434
+ theme, ThemeOptions, ThemeInjected,
2435
+ i18n, I18nOptions, I18nInjected,
2436
+ flash, FlashOptions, FlashInjected,
2252
2437
  ```
2253
2438
 
2254
- ### AI SDK (re-exported from `ai`)
2439
+ ### AI
2255
2440
 
2256
2441
  ```ts
2442
+ aiProvider, AIProvider, AIProviderOptions, AIProviderInjected,
2257
2443
  streamText, generateText, streamObject, generateObject,
2258
2444
  tool, embed, embedMany, smoothStream,
2259
- openai, createOpenAI
2445
+ openai, createOpenAI,
2446
+ aiStream, AIHandler,
2447
+ runWorkflow,
2448
+ agent, AgentModule, AgentOptions,
2449
+ knowledgeBase, KBModule, KBOptions,
2450
+ opencode, OpencodeModule, OpencodeOptions,
2260
2451
  ```
2261
2452
 
2262
- ### Other modules
2453
+ ### API / Routing
2263
2454
 
2264
2455
  ```ts
2265
- theme, i18n, flash, health, analytics, seo, seoMiddleware, seoTags,
2266
- user, mailer, graphql, aiStream, runWorkflow, knowledgeBase, permissions, queue,
2267
- logdb, messager, agent, iii, createWorker, registerWorker,
2268
- opencode, deploy, defineConfig, webhook,
2269
- testApp, TestApp, TestRequest, TestResponse,
2270
- createTestDb, withTestDb,
2271
- getCookies, setCookie, deleteCookie,
2272
- createSSEStream, formatSSE, formatSSEData,
2273
- currentTraceId, currentTrace, runWithTrace, traceElapsed,
2274
- createHub, Hub, HubOptions,
2275
- DEFAULT_MAX_BODY, MIGRATIONS_TABLE,
2276
- fts,
2456
+ analytics, AnalyticsModule, AnalyticsOptions,
2457
+ health, HealthOptions,
2458
+ graphql, GraphQLOptions, GraphQLHandler,
2459
+ logdb, LogdbModule, LogdbOptions,
2460
+ seo, seoMiddleware, seoTags, SeoOptions,
2461
+ webhook, WebhookModule, WebhookOptions,
2462
+ iii, createWorker, registerWorker, IIIModule, IIIOptions,
2463
+ ```
2464
+
2465
+ ### Networking / Storage
2466
+
2467
+ ```ts
2468
+ s3, S3Options, S3Module, S3Body,
2469
+ mailer, MailerOptions, Mailer,
2470
+ messager, MessagerModule, MessagerOptions,
2471
+ hub, createHub, Hub, HubOptions,
2472
+ deploy, defineConfig, DeployConfig, AppConfig,
2473
+ tenant, TenantModule, TenantOptions, TenantContext,
2474
+ ```
2475
+
2476
+ ### Client-side (from `'weifuwu/react'`)
2477
+
2478
+ ```ts
2479
+ TsxContext, setCtx, useCtx, addCtxRebuilder, useLoaderData,
2480
+ useWebsocket, useAction, useFetch, useQueryState, createStore,
2481
+ Link, useNavigate, useNavigating, addInterceptor,
2482
+ useLocale, useTheme, applyTheme, useFlashMessage,
2483
+ useAgentStream,
2484
+ Head,
2485
+
2486
+ // Types:
2487
+ StoreApi,
2488
+ UseActionOptions, UseActionReturn,
2489
+ UseWebsocketOptions, UseWebsocketReturn,
2490
+ UseAgentStreamOptions, UseAgentStreamReturn, AgentStreamState,
2277
2491
  ```
2278
2492
 
2279
2493
  ---