weifuwu 0.24.3 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -210,6 +210,45 @@ app.use('/admin', mw) // path-scoped
210
210
  app.get('/admin', mw, handler) // route-level
211
211
  ```
212
212
 
213
+ ### Middleware Dependency Checking
214
+
215
+ Middleware factories can declare what ctx fields they inject and depend on via
216
+ `__meta`. The Router warns at registration time if a dependency is unsatisfied.
217
+
218
+ ```ts
219
+ // postgres() declares: __meta = { injects: ['sql'], depends: [] }
220
+ // session() declares: __meta = { injects: ['session'], depends: [] }
221
+ // user() declares: __meta = { injects: ['user'], depends: ['sql', 'session'] }
222
+
223
+ const app = new Router()
224
+ app.use(user()) // ⚠️ Warns: depends on 'sql' and 'session' but they aren't registered
225
+ // → "[weifuwu] Middleware at "global" depends on ctx.sql but it hasn't been registered yet."
226
+ // → "Register the provider before this middleware: app.use(sql())"
227
+
228
+ // Correct order:
229
+ app.use(postgres())
230
+ app.use(session())
231
+ app.use(user())
232
+ ```
233
+
234
+ To add `__meta` to your own middleware:
235
+
236
+ ```ts
237
+ function myMiddleware() {
238
+ const mw = async (req, ctx, next) => {
239
+ ctx.myField = await setup()
240
+ return next(req, ctx)
241
+ }
242
+ mw.__meta = { injects: ['myField'], depends: ['sql'] }
243
+ return mw
244
+ }
245
+ ```
246
+
247
+ The check is purely advisory — warnings go to `console.warn`, no errors are thrown. Built-in
248
+ middleware (`postgres`, `redis`, `session`, `aiProvider`, `rateLimit`) all have `__meta` pre-attached.
249
+
250
+ _New in v0.25._
251
+
213
252
  ### Context
214
253
 
215
254
  The `ctx` object accumulates properties as it passes through the middleware chain. Below are all documented properties:
@@ -236,6 +275,7 @@ The `ctx` object accumulates properties as it passes through the middleware chai
236
275
  | `tenant` | `tenant()` | `TenantContext` | Current tenant info |
237
276
  | `parsed` | `validate()` / `upload()` | `{ body, files }` | Validated/parsed request data |
238
277
  | `layoutStack` | `ssr()` internal | `LayoutEntry[]` | React layout component stack |
278
+ | `notifier` | `notifier()` | `Notifier` | Multi-channel notification system |
239
279
  | `loaderData` | User middleware | `Record<string, unknown>` | SSR data passed to client |
240
280
  | `mountPath` | `Router` | `string` | Sub-router mount path |
241
281
  | `deploy` | `deploy()` | `{ appName? }` | Deploy gateway info |
@@ -417,6 +457,10 @@ graph TD
417
457
  | **Multi-process deploy** | `deploy()` | γ |
418
458
  | **Distributed functions (iii)** | `iii()` | β |
419
459
  | **Webhook receiver** | `webhook()` | β |
460
+ | **MCP tool integration** | `mcpClient()` | γ |
461
+ | **Notifications** | `notifier()` | α |
462
+ | **API Key management** | `user({ apiKeys: true })` | β |
463
+ | **WebSocket testing** | `testApp().wsReq()` | — |
420
464
  | **Social login (OAuth)** | `user({ oauthLogin })` | β |
421
465
  | **Database migrations** | `pg.migrate()` | — |
422
466
 
@@ -521,6 +565,47 @@ assert.deepEqual(await res.json(), { id: '42', user: { id: 1 } })
521
565
  | `.header(k,v)` `.body(data)` `.rawBody(str)` | Set request properties |
522
566
  | `.send()` → `TestResponse` | Execute and get `{ status, headers, json(), text() }` |
523
567
 
568
+ **WebSocket testing** (new in v0.25) — `app.ws()` + `app.wsReq()`:
569
+
570
+ ```ts
571
+ const app = testApp()
572
+ app.ws('/echo', {
573
+ open(ws) {
574
+ ws.send(JSON.stringify({ type: 'connected' }))
575
+ },
576
+ message(ws, ctx, data) {
577
+ ws.send('echo: ' + data.toString())
578
+ },
579
+ })
580
+
581
+ // Connect via WebSocket
582
+ const conn = await app.wsReq('/echo').connect()
583
+
584
+ // Wait for the open message
585
+ const openMsg = await conn.receiveJson()
586
+ assert.equal(openMsg.type, 'connected')
587
+
588
+ // Send and receive
589
+ conn.send('hello')
590
+ const reply = await conn.receive()
591
+ assert.equal(reply, 'echo: hello')
592
+
593
+ conn.close()
594
+ await app.close() // cleanup server
595
+ ```
596
+
597
+ | Method | Description |
598
+ | ------------------------------------------ | ------------------------------------------- |
599
+ | `app.ws(path, handler)` | Register a WebSocket handler |
600
+ | `app.wsReq(path)` | Start building a WebSocket connection |
601
+ | `.timeout(ms)` | Set connection timeout (default: 5000) |
602
+ | `.connect()` → `TestWSConnection` | Connect and return a connection handle |
603
+ | `conn.send(data)` / `conn.json(obj)` | Send a message |
604
+ | `conn.receive()` / `conn.receiveJson<T>()` | Wait for the next message |
605
+ | `conn.expectSilent(ms)` | Assert no message arrives within the period |
606
+ | `conn.close()` | Close the connection |
607
+ | `app.close()` | Close all connections and stop the server |
608
+
524
609
  ### Database test isolation
525
610
 
526
611
  ```ts
@@ -1119,6 +1204,51 @@ await mail.send({
1119
1204
  | `from` | `string` | — | Default sender address |
1120
1205
  | `send` | `function` | — | Custom send function (alternative to transport) |
1121
1206
 
1207
+ ### mcpClient [γ] — MCP Server integration [AI]
1208
+
1209
+ [Model Context Protocol](https://modelcontextprotocol.io) client. Spawns MCP server
1210
+ subprocesses and exposes their tools as AI SDK-compatible tool objects.
1211
+
1212
+ ```ts
1213
+ import { mcpClient, agent, aiProvider } from 'weifuwu'
1214
+
1215
+ const fsMcp = mcpClient({
1216
+ command: 'npx',
1217
+ args: ['@modelcontextprotocol/server-filesystem', '/workspace'],
1218
+ })
1219
+
1220
+ const tools = await fsMcp.getTools()
1221
+
1222
+ const a = agent({ pg, provider: aiProvider(), tools })
1223
+ await a.run(agentId, { input: 'read package.json' })
1224
+
1225
+ // Later, refresh tools if the server provides new ones
1226
+ await fsMcp.refresh()
1227
+
1228
+ // Or call a tool directly
1229
+ const result = await fsMcp.callTool('echo', { text: 'hello' })
1230
+
1231
+ await fsMcp.close() // shutdown the MCP server process
1232
+ ```
1233
+
1234
+ | Option | Type | Default | Description |
1235
+ | ----------------- | ---------- | ------- | ------------------------------------------------------- |
1236
+ | `command` | `string` | — | **Required.** Command to spawn (e.g. `'npx'`, `'node'`) |
1237
+ | `args` | `string[]` | `[]` | Arguments passed to the command |
1238
+ | `env` | `object` | — | Extra environment variables |
1239
+ | `timeout` | `number` | `15000` | Handshake/response timeout (ms) |
1240
+ | `maxResponseSize` | `number` | `10MB` | Max tool response body size |
1241
+
1242
+ | Method | Description |
1243
+ | ------------ | ------------------------------------------------------------------------- |
1244
+ | `getTools()` | Fetch tool definitions, returns `Record<string, Tool>`-compatible objects |
1245
+ | `refresh()` | Re-fetch tool definitions from the server |
1246
+ | `callTool()` | Call a tool by name directly |
1247
+ | `close()` | Shutdown the MCP server process |
1248
+
1249
+ Tool schemas (JSON Schema) are automatically converted to Zod schemas for AI SDK compatibility.
1250
+ Responses are concatenated from text content items, with size limiting.
1251
+
1122
1252
  ### oauthLogin (via user()) — Social login (OAuth 2.0 client) [Security]
1123
1253
 
1124
1254
  Social login is built into the [`user()`](#user-β) module via the `oauthLogin` option — no separate import needed.
@@ -1208,6 +1338,72 @@ await msg.send(channelId, 'System message', { sender_type: 'system', sender_id:
1208
1338
  | `.send(channel, content, opts?)` | Send message to channel |
1209
1339
  | `.close()` | Cleanup |
1210
1340
 
1341
+ ### notifier [α] [UX]
1342
+
1343
+ Multi-channel notification system with inbox (DB persistent), email, and
1344
+ WebSocket push. Per-user channel preferences.
1345
+
1346
+ ```ts
1347
+ import { notifier, mailer } from 'weifuwu'
1348
+
1349
+ const mail = mailer({ from: 'noreply@example.com', transport: '...' })
1350
+ const n = notifier({ sql: pg.sql, mailer: mail })
1351
+ await n.migrate()
1352
+ app.use(n) // injects ctx.notifier
1353
+
1354
+ // Send a notification (routes through user's channel preferences)
1355
+ await ctx.notifier.send(
1356
+ { userId: 42, email: 'user@example.com' },
1357
+ { title: 'Welcome!', body: 'Thanks for joining', type: 'onboarding' },
1358
+ )
1359
+
1360
+ // Broadcast to all users with inbox enabled
1361
+ await ctx.notifier.broadcast({
1362
+ title: 'System maintenance tonight',
1363
+ body: 'The system will be down from 2-4 AM',
1364
+ })
1365
+
1366
+ // Check unread count
1367
+ const count = await ctx.notifier.unreadCount(userId)
1368
+
1369
+ // List notifications (newest first)
1370
+ const notifications = await ctx.notifier.list(userId, { limit: 10 })
1371
+
1372
+ // Mark as read
1373
+ await ctx.notifier.markRead(userId, [notifId])
1374
+ await ctx.notifier.markRead(userId) // mark ALL as read
1375
+
1376
+ // User preferences
1377
+ await ctx.notifier.setPreferences(userId, { channels: ['inbox', 'email'] })
1378
+ const prefs = await ctx.notifier.getPreferences(userId)
1379
+ // → { channels: ['inbox', 'email'] }
1380
+ ```
1381
+
1382
+ | Option | Type | Default | Description |
1383
+ | ---------- | ----------- | ------------------ | ------------------------------- |
1384
+ | `sql` | `SqlClient` | — | **Required.** PostgreSQL client |
1385
+ | `mailer` | `Mailer` | — | Mailer for email channel |
1386
+ | `hub` | `Hub` | — | Pub/sub hub for WebSocket push |
1387
+ | `table` | `string` | `'_notifications'` | Notifications table name |
1388
+ | `pageSize` | `number` | `50` | Default page size for list() |
1389
+
1390
+ | Method | Description |
1391
+ | -------------------------------- | --------------------------------------------- |
1392
+ | `.send(to, message)` | Send notification (routes by user preference) |
1393
+ | `.broadcast(message)` | Send to all users with inbox enabled |
1394
+ | `.unreadCount(userId)` | Count unread notifications |
1395
+ | `.count(userId, unreadOnly?)` | Total or unread count |
1396
+ | `.markRead(userId, ids?)` | Mark notification(s) as read |
1397
+ | `.list(userId, opts?)` | List notifications (paginated) |
1398
+ | `.getPreferences(userId)` | Get user's channel preferences |
1399
+ | `.setPreferences(userId, prefs)` | Set user's channel preferences |
1400
+ | `.migrate()` | Create tables |
1401
+ | `.clean(days)` | Delete notifications older than N days |
1402
+
1403
+ **Channel routing:** Each user has channel preferences (default: `['inbox']`). When
1404
+ `sending`, the notification is delivered to each enabled channel. Email requires
1405
+ `mailer` to be configured. WebSocket requires `hub` (e.g. from `messager.wsHandler()`).
1406
+
1211
1407
  ### opencode [β] [AI]
1212
1408
 
1213
1409
  AI programming assistant.
@@ -1986,6 +2182,35 @@ app.use(u.middleware()) // ctx.user
1986
2182
  | `.verify(token)` | Verify JWT token |
1987
2183
  | `.middleware()` | JWT verify middleware — sets `ctx.user` |
1988
2184
 
2185
+ **API Key management** — enable via `user({ apiKeys: true })`:
2186
+
2187
+ ```ts
2188
+ const auth = user({ pg, jwtSecret: process.env.JWT_SECRET, apiKeys: true })
2189
+ await auth.migrate()
2190
+ app.use(auth)
2191
+
2192
+ // Create an API key (server-side)
2193
+ const { id, key } = await auth.createApiKey(userId, 'Deploy Key', ['read', 'deploy'])
2194
+ // key → 'sk_live_abc123...' (only shown once!)
2195
+
2196
+ // List keys (masked)
2197
+ const keys = await auth.listApiKeys(userId)
2198
+ // → [{ id, name, prefix: 'sk_live_abc...f3a2', scopes: ['read','deploy'], last_used_at, revoked }]
2199
+
2200
+ // Revoke a key
2201
+ await auth.revokeApiKey(userId, keyId)
2202
+
2203
+ // REST API (auto-mounted when routes are registered)
2204
+ // POST /api-keys → Create (requires auth)
2205
+ // GET /api-keys → List (requires auth)
2206
+ // DELETE /api-keys/:id → Revoke (requires auth)
2207
+ ```
2208
+
2209
+ API keys start with `sk_live_` and are hashed with SHA256 before storage.
2210
+ The middleware resolves API keys automatically — use them with `Authorization: Bearer sk_live_...`.
2211
+
2212
+ _New in v0.25._
2213
+
1989
2214
  ### permissions [α] — RBAC [Security]
1990
2215
 
1991
2216
  Role-based access control.
@@ -1,2 +1,2 @@
1
1
  import type { IIIModule, IIIOptions } from './types.ts';
2
- export declare function iii(opts?: IIIOptions): IIIModule;
2
+ export declare function iii(_opts?: IIIOptions): IIIModule;
@@ -5,6 +5,5 @@ export declare function registerWorker(url: string): {
5
5
  registerTrigger(input: TriggerInput): void;
6
6
  unregisterTrigger(functionId: string): void;
7
7
  trigger(request: TriggerRequest): Promise<unknown>;
8
- onStream(handler: (data: any) => void): void;
9
8
  close(): void;
10
9
  };
@@ -1,15 +1,15 @@
1
1
  import type { Router } from '../router.ts';
2
2
  import type { Closeable } from '../types.ts';
3
- import type { Redis } from '../vendor.ts';
4
3
  import type { PostgresClient } from '../postgres/types.ts';
4
+ /** A function handler receives a payload and returns a result. */
5
5
  export type FunctionHandler = (payload: unknown, ctx: FunctionContext) => unknown | Promise<unknown>;
6
6
  export interface FunctionContext {
7
7
  engine: IIIModule;
8
8
  functionId: string;
9
9
  workerName: string;
10
- triggerId?: string;
11
10
  user?: unknown;
12
11
  }
12
+ /** Optional metadata for a trigger — iii does NOT schedule triggers, just stores them for reference. */
13
13
  export interface TriggerInput {
14
14
  type: string;
15
15
  function_id: string;
@@ -17,9 +17,6 @@ export interface TriggerInput {
17
17
  }
18
18
  export interface IIIOptions {
19
19
  pg?: PostgresClient;
20
- redis?: Redis;
21
- /** TTL in seconds for Redis stream keys. Default: 3600 (1 hour). Set to 0 for no expiration. */
22
- streamTTL?: number;
23
20
  }
24
21
  export interface TriggerRequest {
25
22
  function_id: string;
@@ -27,19 +24,24 @@ export interface TriggerRequest {
27
24
  action?: 'sync' | 'void';
28
25
  timeout_ms?: number;
29
26
  }
30
- export interface TriggerOptions {
31
- action?: 'sync' | 'void';
32
- timeout_ms?: number;
33
- }
27
+ /** III (跨进程函数调用) module — register remote workers and call functions across processes. */
34
28
  export interface IIIModule extends Router, Closeable {
35
29
  wsHandler: () => any;
30
+ /** Register a local worker. Its functions become callable via trigger(). */
36
31
  addWorker: (worker: Worker) => void;
32
+ /** Call a function by ID. Returns the function's result. */
37
33
  trigger: (request: TriggerRequest) => Promise<unknown>;
34
+ /** Remove a previously registered worker. */
38
35
  removeWorker: (worker: Worker) => void;
36
+ /** List all registered workers. */
39
37
  listWorkers: () => WorkerInfo[];
38
+ /** List all registered functions. */
40
39
  listFunctions: () => FunctionInfo[];
40
+ /** List all registered trigger metadata. */
41
41
  listTriggers: () => TriggerInfo[];
42
+ /** Create the database tables (currently no-op, kept for API compatibility). */
42
43
  migrate: () => Promise<void>;
44
+ /** Shutdown — reject pending invocations, disconnect all workers. */
43
45
  close: () => Promise<void>;
44
46
  }
45
47
  export interface WorkerInfo {
@@ -63,30 +65,37 @@ export interface TriggerInfo {
63
65
  config: Record<string, unknown>;
64
66
  workerId: string;
65
67
  }
68
+ /** A worker — a group of related functions. */
66
69
  export interface Worker {
67
70
  readonly name: string;
71
+ /** Register a function that can be called remotely. */
68
72
  registerFunction: (id: string, handler: FunctionHandler, opts?: {
69
73
  description?: string;
70
74
  }) => Worker;
75
+ /** Unregister a function. */
71
76
  unregisterFunction: (id: string) => Worker;
77
+ /** Attach metadata (the module does NOT schedule — it's stored for introspection). */
72
78
  registerTrigger: (input: TriggerInput) => Worker;
79
+ /** Remove trigger metadata. */
73
80
  unregisterTrigger: (functionId: string) => Worker;
81
+ /** Get all registered functions. */
74
82
  getFunctions: () => {
75
83
  id: string;
76
84
  handler: FunctionHandler;
77
85
  }[];
86
+ /** Get all registered trigger metadata. */
78
87
  getTriggers: () => {
79
88
  id: string;
80
89
  input: TriggerInput;
81
90
  }[];
82
91
  }
92
+ /** A handle to interact with a remote worker from the client side. */
83
93
  export interface RemoteWorker {
84
94
  registerFunction: (id: string, handler: FunctionHandler) => void;
85
95
  unregisterFunction: (id: string) => void;
86
96
  registerTrigger: (input: TriggerInput) => void;
87
97
  unregisterTrigger: (functionId: string) => void;
88
98
  trigger: (request: TriggerRequest) => Promise<unknown>;
89
- /** Cleanup worker resources. */
90
99
  close: () => void;
91
100
  }
92
101
  export interface FunctionRegistration {
@@ -110,26 +119,3 @@ export interface WorkerRegistration {
110
119
  functions: FunctionRegistration[];
111
120
  triggers: TriggerRegistration[];
112
121
  }
113
- export type StreamUpdateOp = {
114
- op: 'set';
115
- value: unknown;
116
- } | {
117
- op: 'merge';
118
- value: Record<string, unknown>;
119
- } | {
120
- op: 'increment';
121
- value: number;
122
- } | {
123
- op: 'decrement';
124
- value: number;
125
- } | {
126
- op: 'append';
127
- value: unknown;
128
- } | {
129
- op: 'remove';
130
- };
131
- export interface StreamSubscription {
132
- stream_name: string;
133
- group_id?: string;
134
- item_id?: string;
135
- }
package/dist/iii/ws.d.ts CHANGED
@@ -10,12 +10,6 @@ interface WsHandlerDeps {
10
10
  config: Record<string, unknown>;
11
11
  }) => void;
12
12
  unregisterRemoteTrigger: (workerId: string, functionId: string) => void;
13
- addStreamSubscriber: (ws: WebSocket, sub: {
14
- stream_name: string;
15
- group_id?: string;
16
- item_id?: string;
17
- }) => void;
18
- removeStreamSubscriber: (ws: WebSocket) => void;
19
13
  handleInvokeResult: (invocationId: string, result: unknown) => void;
20
14
  handleInvokeError: (invocationId: string, error: string) => void;
21
15
  handleInvoke: (ws: WebSocket, invocationId: string, functionId: string, payload: unknown) => void;
@@ -24,6 +18,5 @@ export declare function createWsHandler(deps: WsHandlerDeps): {
24
18
  open(_ws: WebSocket, _ctx: Context): void;
25
19
  message(ws: WebSocket, ctx: Context, data: string | Buffer): Promise<void>;
26
20
  close(ws: WebSocket): void;
27
- error(ws: WebSocket): void;
28
21
  };
29
22
  export {};
package/dist/index.d.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  export type { Context, Handler, Middleware, ErrorHandler } from './types.ts';
2
+ export { HttpError } from './types.ts';
2
3
  export { currentTraceId, currentTrace, runWithTrace, traceElapsed, trace } from './trace.ts';
3
4
  export type { TraceContext, TraceInjected, TraceOptions } from './trace.ts';
4
5
  export { loadEnv, isDev, isProd, isBundled, getPublicEnv, env } from './env.ts';
@@ -42,7 +43,7 @@ export { streamText, generateText, generateObject, streamObject, tool, embed, em
42
43
  export { postgres, MIGRATIONS_TABLE } from './postgres/index.ts';
43
44
  export type { PostgresOptions, PostgresClient, PostgresInjected } from './postgres/types.ts';
44
45
  export { user } from './user/index.ts';
45
- export type { UserOptions, UserData, UserModule, OAuth2Client, OAuthProviderConfig, } from './user/types.ts';
46
+ export type { UserOptions, UserData, UserModule, OAuth2Client, OAuthProviderConfig, ApiKeyInfo, } from './user/types.ts';
46
47
  export type { UserInjected } from './user/types.ts';
47
48
  export { redis } from './redis/index.ts';
48
49
  export type { RedisOptions, RedisClient, RedisInjected } from './redis/types.ts';
@@ -94,3 +95,7 @@ export { knowledgeBase } from './kb/index.ts';
94
95
  export type { KBOptions, KBIngestOptions, KBSearchResult, KBSearchOptions, KBListEntry, KBModule, } from './kb/types.ts';
95
96
  export { permissions } from './permissions.ts';
96
97
  export type { PermissionsOptions, PermissionsModule } from './permissions.ts';
98
+ export { mcpClient } from './mcp.ts';
99
+ export type { MCPClient, MCPClientOptions, MCPToolDef } from './mcp.ts';
100
+ export { notifier } from './notifier/index.ts';
101
+ export type { NotifierOptions, Notifier, NotifierInjected, NotifyMessage, Notification, NotifyChannel, NotifyPreferences, } from './notifier/types.ts';