weifuwu 0.31.2 → 0.32.0

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
@@ -64,7 +64,8 @@ app.get('/users/:id', (req, ctx) => {
64
64
  | `logger(opts?)` | Request logging. `format: 'short' | 'combined' | 'json'`. |
65
65
  | `upload(opts?)` | Multipart file upload via `req.formData()`. Injects `ctx.parsed`. |
66
66
  | `serveStatic(root, opts?)` | Static file handler. `cacheControl`, `index`. |
67
- | `trace(opts?)` | Injects `ctx.trace = { requestId, traceId, elapsed(), startTime }`. |
67
+ | `sandbox(opts?)` | Filesystem isolation for agent operations. `baseDir`, `timeout`, `isolateBy`. |
68
+ | `auth(opts?)` | Authentication: JWT, session cookies, API keys. Injects `ctx.user`. |
68
69
 
69
70
  ### Tracing
70
71
 
@@ -313,6 +314,108 @@ import { createBrowserRouter, hydrate, navigate } from 'weifuwu/react/client'
313
314
 
314
315
  See [examples/react-ssr/](examples/react-ssr/) for the full demo.
315
316
 
317
+ ### AI Agent
318
+
319
+ > Requires `ai` (Vercel AI SDK). Install with a model provider:
320
+ > ```bash
321
+ > npm install ai @ai-sdk/openai
322
+ > ```
323
+
324
+ ```ts
325
+ import { agent } from 'weifuwu'
326
+ import { openai } from '@ai-sdk/openai'
327
+ import { tool } from 'ai'
328
+ import { z } from 'zod'
329
+
330
+ app.use(agent({
331
+ model: openai('gpt-4o'),
332
+ system: 'You are a helpful assistant.',
333
+ knowledge: {
334
+ // RAG — search a knowledge base before each response
335
+ search: async (query, ctx) => {
336
+ const { embeddings } = await embedModel.doEmbed({ values: [query] })
337
+ return ctx.sql`
338
+ SELECT content, 1 - (embedding <=> ${embeddings[0]}::vector) AS score
339
+ FROM docs ORDER BY embedding <=> ${embeddings[0]}::vector LIMIT 3
340
+ `
341
+ },
342
+ },
343
+ tools: {
344
+ getWeather: tool({
345
+ description: 'Get weather for a city',
346
+ parameters: z.object({ city: z.string() }),
347
+ execute: async ({ city }) => ({ temp: 22, unit: 'C' }),
348
+ }),
349
+ },
350
+ maxSteps: 5,
351
+ }))
352
+
353
+ app.post('/api/chat', async (req, ctx) => {
354
+ const { messages } = await req.json()
355
+ return ctx.agent.chatStreamResponse({ messages })
356
+ })
357
+ ```
358
+
359
+ **`agent()` handles:** multi-turn conversations, automatic tool-calling loops (maxSteps), knowledge retrieval (RAG) injected into the system prompt, and SSE streaming compatible with `useChat` from `@ai-sdk/react`.
360
+
361
+ | Feature | Description |
362
+ |---|---|
363
+ | `ctx.agent.chat(prompt, opts?)` | Non-streaming chat with tool calling and RAG |
364
+ | `ctx.agent.chatStreamResponse({ messages })` | SSE streaming response (useChat-compatible) |
365
+ | `knowledge.search` | User-defined RAG callback — query any data source via `ctx` |
366
+ | `tools` | Tool definitions (from `ai` package). Executed in automatic loops |
367
+ | `agents` | Named sub-agents with different models/tools |
368
+ | `sandbox: true` | Auto-integrate with `ctx.sandbox` for file operations |
369
+ | `store` | Session persistence (save/load conversation history) |
370
+
371
+ ### Auth
372
+
373
+ ```ts
374
+ import { auth } from 'weifuwu'
375
+
376
+ // JWT
377
+ app.use(auth({ jwt: { secret: process.env.JWT_SECRET } }))
378
+
379
+ // Session cookie
380
+ app.use(auth({
381
+ session: {
382
+ secret: '...',
383
+ loadUser: async (data) => db.findUser(data.userId),
384
+ },
385
+ }))
386
+
387
+ // API key
388
+ app.use(auth({ apiKey: { validate: async (key) => db.findByApiKey(key) } }))
389
+
390
+ // ctx.user is now available
391
+ app.get('/me', (req, ctx) => {
392
+ if (!ctx.user) return new Response('Unauthorized', { status: 401 })
393
+ return Response.json(ctx.user)
394
+ })
395
+ ```
396
+
397
+ Supports JWT (HS256 via cookie or Authorization header), signed session cookies with `loadUser` callback, and API key validation. All are optional — requests proceed without a user identity unless handlers enforce it.
398
+
399
+ ### Sandbox
400
+
401
+ ```ts
402
+ import { sandbox } from 'weifuwu'
403
+
404
+ app.use(sandbox({
405
+ baseDir: '/tmp/workspaces',
406
+ timeout: 30000,
407
+ isolateBy: 'user', // one directory per ctx.user.id
408
+ }))
409
+
410
+ // ctx.sandbox provides isolated file + exec operations
411
+ await ctx.sandbox.writeFile('hello.txt', 'world')
412
+ const content = await ctx.sandbox.readFile('hello.txt')
413
+ const { stdout } = await ctx.sandbox.exec('ls -la')
414
+ await ctx.sandbox.destroy() // clean up workspace
415
+ ```
416
+
417
+ All file paths are validated — escapes (`../`) are rejected. `exec()` enforces timeout and sets `HOME` to the workspace directory. When `isolateBy: 'user'` is set, each user gets their own directory under `baseDir`.
418
+
316
419
  ### Types
317
420
 
318
421
  | Export | Description |
@@ -368,50 +471,35 @@ app.onError((err, req, ctx) => {
368
471
  ## Complete example
369
472
 
370
473
  ```ts
371
- import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, queue, HttpError } from 'weifuwu'
474
+ import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, auth, sandbox, HttpError } from 'weifuwu'
475
+ import { agent } from 'weifuwu/agent'
476
+ import { react } from 'weifuwu/react'
477
+ import { openai } from '@ai-sdk/openai'
372
478
 
373
479
  const app = new Router()
480
+ .use(trace())
481
+ .use(logger())
482
+ .use(cors())
483
+ .use(helmet())
484
+ .use(compress())
485
+ .use(postgres())
486
+ .use(redis())
487
+ .use(auth({ jwt: { secret: process.env.JWT_SECRET! } }))
488
+ .use(sandbox({ isolateBy: 'user' }))
489
+ .use(agent({
490
+ model: openai('gpt-4o'),
491
+ system: 'You are a helpful assistant.',
492
+ knowledge: {
493
+ search: async (q, ctx) => ctx.sql`...`,
494
+ },
495
+ sandbox: true,
496
+ }))
497
+ .plugin(react({ pages: { '/': './Chat.tsx' }, layout: './Layout.tsx', tailwind: {} }))
374
498
 
375
- // Global middleware
376
- app.use(trace())
377
- app.use(logger())
378
- app.use(cors())
379
- app.use(helmet())
380
- app.use(compress())
381
- app.use(rateLimit({ windowMs: 60_000, max: 100 }))
382
-
383
- // Database
384
- const sql = postgres()
385
- app.use(sql)
386
-
387
- // Routes
388
- app.get('/api/users', async (req, ctx) => {
389
- const users = await ctx.sql`SELECT id, name FROM users ORDER BY id`
390
- return Response.json(users)
391
- })
392
-
393
- app.post('/api/users', async (req, ctx) => {
394
- const { name, email } = await req.json()
395
- const [user] = await ctx.sql`INSERT INTO users (name,email) VALUES (${name},${email}) RETURNING *`
396
- return Response.json(user, { status: 201 })
397
- })
398
-
399
- app.get('/api/users/:id', async (req, ctx) => {
400
- const [user] = await ctx.sql`SELECT * FROM users WHERE id = ${ctx.params.id}`
401
- if (!user) throw new HttpError('Not found', 404)
402
- return Response.json(user)
403
- })
404
-
405
- // WebSocket
406
- app.ws('/chat', {
407
- open(ws, ctx) { ctx.ws.join('lobby') },
408
- message(ws, ctx, data) { ctx.ws.sendRoom('lobby', { text: data.toString() }) },
409
- })
410
-
411
- // Error handling
412
- app.onError((err) => {
413
- if (err instanceof HttpError) return Response.json({ error: err.message }, { status: err.status })
414
- return Response.json({ error: 'Internal error' }, { status: 500 })
499
+ app.post('/api/chat', async (req, ctx) => {
500
+ if (!ctx.user) return new Response('Unauthorized', { status: 401 })
501
+ const { messages } = await req.json()
502
+ return ctx.agent.chatStreamResponse({ messages })
415
503
  })
416
504
 
417
505
  serve(app, { port: 3000 })
@@ -432,6 +520,7 @@ weifuwu/
432
520
  │ ├── types.ts
433
521
  │ ├── core/ ← serve, router, ws, trace, logger
434
522
  │ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
523
+ │ ├── ai/ ← AI + agent middleware
435
524
  │ ├── postgres/
436
525
  │ ├── redis/
437
526
  │ ├── react/ ← react SSR (render, navigation, client)
@@ -0,0 +1,127 @@
1
+ import type { Middleware, Context } from '../types.ts';
2
+ import type { AiOptions } from '../ai/types.ts';
3
+ declare module '../types.ts' {
4
+ interface Context {
5
+ /**
6
+ * AI agent — LLM integration with session management, tool calling,
7
+ * knowledge retrieval, and streaming.
8
+ * Injected by the `agent()` middleware.
9
+ */
10
+ agent: Agent;
11
+ }
12
+ }
13
+ export interface Agent {
14
+ /**
15
+ * Chat with the agent (non-streaming). Returns the final response
16
+ * after tool calls and knowledge retrieval.
17
+ */
18
+ chat(prompt: string, opts?: AgentChatOptions): Promise<AgentChatResult>;
19
+ /**
20
+ * Stream the agent's response as an SSE Response.
21
+ * Compatible with `useChat` from @ai-sdk/react.
22
+ */
23
+ chatStreamResponse(opts: AgentStreamOptions): Response;
24
+ /** The underlying language model. */
25
+ model: unknown;
26
+ /** Default system prompt. */
27
+ system: string | undefined;
28
+ }
29
+ export interface AgentChatOptions {
30
+ /** Override system prompt for this call. */
31
+ system?: string;
32
+ /** Chat history messages (from useChat). */
33
+ messages?: Array<{
34
+ role: string;
35
+ content: string;
36
+ [key: string]: unknown;
37
+ }>;
38
+ /** Override temperature. */
39
+ temperature?: number;
40
+ /** Override max tokens. */
41
+ maxTokens?: number;
42
+ /** Override max steps. */
43
+ maxSteps?: number;
44
+ }
45
+ export interface AgentStreamOptions {
46
+ /** User prompt or full message list. */
47
+ messages: Array<{
48
+ role: string;
49
+ content: string;
50
+ [key: string]: unknown;
51
+ }>;
52
+ /** Override system prompt. */
53
+ system?: string;
54
+ }
55
+ export type AgentChatResult = Record<string, any>;
56
+ export interface AgentOptions extends AiOptions {
57
+ /**
58
+ * Knowledge base for RAG. Called before every chat to inject context.
59
+ */
60
+ knowledge?: {
61
+ /**
62
+ * Search for relevant documents given a user query.
63
+ * Results are injected into the system prompt.
64
+ */
65
+ search: (query: string, ctx: Context) => Promise<Array<{
66
+ content: string;
67
+ score?: number;
68
+ }>>;
69
+ /** Maximum results to inject (default: 3). */
70
+ topK?: number;
71
+ /** Minimum relevance score (default: 0). */
72
+ minScore?: number;
73
+ };
74
+ /**
75
+ * Named sub-agents with their own model, system prompt, and tools.
76
+ * Access via ctx.agent.agents[name].
77
+ */
78
+ agents?: Record<string, {
79
+ model?: unknown;
80
+ system?: string;
81
+ tools?: Record<string, unknown>;
82
+ }>;
83
+ /**
84
+ * Enable sandbox integration. When true and ctx.sandbox is available,
85
+ * the agent automatically uses it for file operations.
86
+ */
87
+ sandbox?: boolean;
88
+ /**
89
+ * Store for session persistence. Save/load conversation history.
90
+ */
91
+ store?: {
92
+ save: (sessionId: string, messages: unknown[]) => Promise<void>;
93
+ load: (sessionId: string) => Promise<unknown[]>;
94
+ };
95
+ }
96
+ /**
97
+ * AI Agent middleware — injects `ctx.agent` for LLM-powered conversations.
98
+ *
99
+ * Built on the Vercel AI SDK. Supports:
100
+ * - Multi-turn conversations with session history
101
+ * - Tool calling with automatic loops (maxSteps)
102
+ * - Knowledge retrieval (RAG) via `knowledge.search`
103
+ * - Streaming SSE responses compatible with `useChat` from @ai-sdk/react
104
+ * - Named sub-agents for role-based delegation
105
+ * - Sandbox integration
106
+ *
107
+ * @example
108
+ * ```ts
109
+ * import { agent } from 'weifuwu'
110
+ * import { openai } from '@ai-sdk/openai'
111
+ *
112
+ * app.use(agent({
113
+ * model: openai('gpt-4o'),
114
+ * system: 'You are a helpful assistant.',
115
+ * knowledge: {
116
+ * search: async (query, ctx) => ctx.sql`...pgvector...`,
117
+ * },
118
+ * tools: { getWeather: tool({...}) },
119
+ * }))
120
+ *
121
+ * app.post('/api/chat', async (req, ctx) => {
122
+ * const { messages } = await req.json()
123
+ * return ctx.agent.chatStreamResponse({ messages })
124
+ * })
125
+ * ```
126
+ */
127
+ export declare function agent(opts: AgentOptions): Middleware;
@@ -0,0 +1,26 @@
1
+ import type { Middleware } from '../types.ts';
2
+ import type { AiOptions } from './types.ts';
3
+ export type { AiOptions, Ai } from './types.ts';
4
+ /**
5
+ * AI middleware — injects `ctx.ai` for LLM integration via the Vercel AI SDK.
6
+ *
7
+ * Requires a language model provider (e.g. `@ai-sdk/openai`, `@ai-sdk/anthropic`).
8
+ *
9
+ * @example
10
+ * ```ts
11
+ * import { ai } from 'weifuwu'
12
+ * import { openai } from '@ai-sdk/openai'
13
+ *
14
+ * app.use(ai({
15
+ * model: openai('gpt-4o'),
16
+ * system: 'You are a helpful assistant.',
17
+ * }))
18
+ *
19
+ * app.post('/api/chat', async (req, ctx) => {
20
+ * const { prompt } = await req.json()
21
+ * const result = await ctx.ai.generateText({ prompt })
22
+ * return Response.json({ text: result.text })
23
+ * })
24
+ * ```
25
+ */
26
+ export declare function ai(opts: AiOptions): Middleware;
@@ -0,0 +1,59 @@
1
+ declare module '../types.ts' {
2
+ interface Context {
3
+ /** AI / LLM integration. Injected by the `ai()` middleware. */
4
+ ai: Ai;
5
+ }
6
+ }
7
+ export interface Ai {
8
+ /**
9
+ * Generate text (non-streaming). See `generateText` from the `ai` package.
10
+ *
11
+ * @example
12
+ * const { text, toolCalls, steps } = await ctx.ai.generateText({
13
+ * prompt: 'What is the weather?',
14
+ * })
15
+ */
16
+ generateText(opts: GenerateTextParams): Promise<AiGenerateTextResult>;
17
+ /** The language model instance. */
18
+ model: unknown;
19
+ /** Default system prompt. */
20
+ system: string | undefined;
21
+ }
22
+ export type AiGenerateTextResult = Awaited<ReturnType<typeof import('ai').generateText<any, any, any>>>;
23
+ export interface AiOptions {
24
+ /**
25
+ * Language model instance (from @ai-sdk/openai, @ai-sdk/anthropic, etc.).
26
+ *
27
+ * @example
28
+ * import { openai } from '@ai-sdk/openai'
29
+ * ai({ model: openai('gpt-4o') })
30
+ */
31
+ model: unknown;
32
+ /** System prompt added to every request. */
33
+ system?: string;
34
+ /** Tools available for tool calling. */
35
+ tools?: any;
36
+ /** Maximum agent steps for tool-calling loops (default: 1). */
37
+ maxSteps?: number;
38
+ /** Default temperature (0–2). */
39
+ temperature?: number;
40
+ /** Default max output tokens. */
41
+ maxTokens?: number;
42
+ }
43
+ export interface GenerateTextParams {
44
+ /** User prompt. */
45
+ prompt: string;
46
+ /** System prompt override. */
47
+ system?: string;
48
+ /** Chat history messages. */
49
+ messages?: Array<{
50
+ role: string;
51
+ content: string;
52
+ } & Record<string, any>>;
53
+ /** Override temperature. */
54
+ temperature?: number;
55
+ /** Override max tokens. */
56
+ maxTokens?: number;
57
+ /** Override max steps. */
58
+ maxSteps?: number;
59
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  export type { Context, Handler, Middleware, ErrorHandler, WsContext, WebSocket } from './types.ts';
2
2
  export { HttpError } from './types.ts';
3
+ export type { User } from './types.ts';
3
4
  export { currentTraceId, currentTrace, runWithTrace, traceElapsed, trace } from './core/trace.ts';
4
5
  export type { TraceContext, TraceInjected, TraceOptions } from './core/trace.ts';
5
6
  export { serve, DEFAULT_MAX_BODY } from './core/serve.ts';
@@ -10,10 +11,14 @@ export { logger } from './core/logger.ts';
10
11
  export type { LoggerOptions } from './core/logger.ts';
11
12
  export { cors } from './middleware/cors.ts';
12
13
  export type { CORSOptions } from './middleware/cors.ts';
14
+ export { auth } from './middleware/auth.ts';
15
+ export type { AuthOptions } from './middleware/auth.ts';
13
16
  export { serveStatic } from './middleware/static.ts';
14
17
  export type { ServeStaticOptions } from './middleware/static.ts';
15
18
  export { upload } from './middleware/upload.ts';
16
19
  export type { UploadOptions, UploadedFile, UploadModule } from './middleware/upload.ts';
20
+ export { sandbox } from './middleware/sandbox.ts';
21
+ export type { SandboxOptions, Sandbox } from './middleware/sandbox.ts';
17
22
  export { esbuildDev } from './middleware/esbuild-dev.ts';
18
23
  export type { EsbuildDevEntry, EsbuildDevOptions } from './middleware/esbuild-dev.ts';
19
24
  export { tailwindDev } from './middleware/tailwind-dev.ts';
@@ -37,3 +42,7 @@ export type { QueueOptions, QueueJob, Queue, QueueInjected } from './queue/types
37
42
  export { react, reactRouter } from './react/index.ts';
38
43
  export type { ReactOptions, RenderOptions, ReactRouterOptions, ReactAppOptions } from './react/types.ts';
39
44
  export { useServerData, ServerDataContext, Link, ErrorBoundary } from './react/index.ts';
45
+ export { ai } from './ai/index.ts';
46
+ export type { AiOptions, Ai } from './ai/index.ts';
47
+ export { agent } from './ai/agent.ts';
48
+ export type { AgentOptions, Agent } from './ai/agent.ts';
package/dist/index.js CHANGED
@@ -9,7 +9,7 @@ var HttpError = class extends Error {
9
9
  };
10
10
 
11
11
  // src/core/trace.ts
12
- import crypto from "node:crypto";
12
+ import crypto2 from "node:crypto";
13
13
  import { AsyncLocalStorage } from "node:async_hooks";
14
14
  var als = new AsyncLocalStorage();
15
15
  function currentTraceId() {
@@ -19,7 +19,7 @@ function currentTrace() {
19
19
  return als.getStore();
20
20
  }
21
21
  function runWithTrace(incomingTraceId, fn) {
22
- const traceId = incomingTraceId || crypto.randomUUID();
22
+ const traceId = incomingTraceId || crypto2.randomUUID();
23
23
  const startTime = Date.now();
24
24
  return als.run({ traceId, startTime }, fn);
25
25
  }
@@ -30,7 +30,7 @@ function traceElapsed() {
30
30
  }
31
31
  function trace(options) {
32
32
  const header = options?.header ?? "X-Request-ID";
33
- const gen = options?.generator ?? (() => crypto.randomUUID());
33
+ const gen = options?.generator ?? (() => crypto2.randomUUID());
34
34
  return async (req, ctx, next) => {
35
35
  const existing = req.headers.get(header);
36
36
  const requestId = existing ?? gen();
@@ -225,14 +225,14 @@ function serve(router, options) {
225
225
  if (!server.listening) return;
226
226
  server.close();
227
227
  server.closeIdleConnections();
228
- return new Promise((resolve5) => {
228
+ return new Promise((resolve6) => {
229
229
  const timer = setTimeout(() => {
230
230
  server.closeAllConnections();
231
- resolve5();
231
+ resolve6();
232
232
  }, timeoutMs);
233
233
  server.on("close", () => {
234
234
  clearTimeout(timer);
235
- resolve5();
235
+ resolve6();
236
236
  });
237
237
  });
238
238
  }
@@ -277,7 +277,7 @@ function createHub(opts) {
277
277
  }
278
278
  });
279
279
  }
280
- function join3(key, ws) {
280
+ function join4(key, ws) {
281
281
  if (!channels.has(key)) {
282
282
  channels.set(key, /* @__PURE__ */ new Set());
283
283
  redisSub?.subscribe(`${prefix}${key}`);
@@ -339,7 +339,7 @@ function createHub(opts) {
339
339
  redisPub = void 0;
340
340
  redisSub = null;
341
341
  }
342
- return { join: join3, leave, broadcast, close };
342
+ return { join: join4, leave, broadcast, close };
343
343
  }
344
344
 
345
345
  // src/core/ws.ts
@@ -906,6 +906,78 @@ function cors(options) {
906
906
  };
907
907
  }
908
908
 
909
+ // src/middleware/auth.ts
910
+ import { createHmac, timingSafeEqual } from "node:crypto";
911
+ function base64urlDecode(str) {
912
+ return Buffer.from(str.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf-8");
913
+ }
914
+ function sign(payload, secret) {
915
+ return createHmac("sha256", secret).update(payload).digest("base64url");
916
+ }
917
+ function parseCookie(cookieHeader, name) {
918
+ if (!cookieHeader) return null;
919
+ for (const c of cookieHeader.split(";")) {
920
+ const [key, ...rest] = c.trim().split("=");
921
+ if (key === name) return rest.join("=");
922
+ }
923
+ return null;
924
+ }
925
+ function auth(opts) {
926
+ return async (req, ctx, next) => {
927
+ if (opts.jwt) {
928
+ const cookie = opts.jwt.cookie ?? "token";
929
+ const token = parseCookie(req.headers.get("cookie"), cookie) ?? req.headers.get("authorization")?.replace(/^Bearer\s+/i, "");
930
+ if (token) {
931
+ try {
932
+ const [headerB64, payloadB64, sigB64] = token.split(".");
933
+ const expectedSig = sign(`${headerB64}.${payloadB64}`, opts.jwt.secret);
934
+ const sigBuf = Buffer.from(sigB64, "base64url");
935
+ const expectedBuf = Buffer.from(expectedSig, "base64url");
936
+ if (sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf)) {
937
+ const payload = JSON.parse(base64urlDecode(payloadB64));
938
+ ctx.user = {
939
+ id: payload.sub ?? payload.id ?? "unknown",
940
+ role: payload.role,
941
+ tenant: payload.tenant,
942
+ ...payload
943
+ };
944
+ }
945
+ } catch {
946
+ }
947
+ }
948
+ }
949
+ if (opts.session && !ctx.user) {
950
+ const cookie = opts.session.cookie ?? "session";
951
+ const raw = parseCookie(req.headers.get("cookie"), cookie);
952
+ if (raw) {
953
+ try {
954
+ const [dataB64, sigB64] = raw.split(".");
955
+ const expectedSig = sign(dataB64, opts.session.secret);
956
+ const sigBuf = Buffer.from(sigB64, "base64url");
957
+ const expectedBuf = Buffer.from(expectedSig, "base64url");
958
+ if (sigBuf.length === expectedBuf.length && timingSafeEqual(sigBuf, expectedBuf)) {
959
+ const data = JSON.parse(base64urlDecode(dataB64));
960
+ const user = await opts.session.loadUser(data);
961
+ if (user) ctx.user = user;
962
+ }
963
+ } catch {
964
+ }
965
+ }
966
+ }
967
+ if (opts.apiKey && !ctx.user) {
968
+ const header = opts.apiKey.header ?? "authorization";
969
+ const prefix = opts.apiKey.prefix ?? "Bearer ";
970
+ const raw = req.headers.get(header);
971
+ if (raw?.startsWith(prefix)) {
972
+ const key = raw.slice(prefix.length);
973
+ const user = await opts.apiKey.validate(key);
974
+ if (user) ctx.user = user;
975
+ }
976
+ }
977
+ return next(req, ctx);
978
+ };
979
+ }
980
+
909
981
  // src/middleware/static.ts
910
982
  import { open, realpath } from "node:fs/promises";
911
983
  import { extname, resolve, normalize, sep } from "node:path";
@@ -1113,9 +1185,77 @@ function upload(options) {
1113
1185
  return mw;
1114
1186
  }
1115
1187
 
1188
+ // src/middleware/sandbox.ts
1189
+ import { join as join2, resolve as resolve2, normalize as normalize2 } from "node:path";
1190
+ import { mkdir as mkdir2, readFile, writeFile as writeFile2, rm } from "node:fs/promises";
1191
+ import { exec as cpExec } from "node:child_process";
1192
+ import { promisify } from "node:util";
1193
+ var runExec = promisify(cpExec);
1194
+ function sandbox(opts = {}) {
1195
+ const base = resolve2(opts.baseDir ?? "/tmp/weifuwu-sandboxes");
1196
+ const defaultTimeout = opts.timeout ?? 3e4;
1197
+ return async (req, ctx, next) => {
1198
+ let sessionId;
1199
+ if (opts.isolateBy === "user" && ctx.user) {
1200
+ sessionId = ctx.user.id ?? "default";
1201
+ } else {
1202
+ sessionId = crypto.randomUUID();
1203
+ }
1204
+ const workDir = join2(base, encodeURIComponent(sessionId));
1205
+ await mkdir2(workDir, { recursive: true });
1206
+ function safePath(p) {
1207
+ const resolved = resolve2(workDir, normalize2(p).replace(/^[/\\]+/, ""));
1208
+ if (!resolved.startsWith(workDir + "/") && resolved !== workDir) {
1209
+ throw new Error(`Path escape rejected: ${p}`);
1210
+ }
1211
+ return resolved;
1212
+ }
1213
+ ctx.sandbox = {
1214
+ workDir,
1215
+ async readFile(path) {
1216
+ return readFile(safePath(path), "utf-8");
1217
+ },
1218
+ async writeFile(path, content) {
1219
+ const resolved = safePath(path);
1220
+ await mkdir2(join2(resolved, ".."), { recursive: true });
1221
+ return writeFile2(resolved, content, "utf-8");
1222
+ },
1223
+ async exec(cmd, execOpts) {
1224
+ const cwd = execOpts?.cwd ? safePath(execOpts.cwd) : workDir;
1225
+ const { stdout, stderr } = await runExec(cmd, {
1226
+ cwd,
1227
+ timeout: execOpts?.timeout ?? defaultTimeout,
1228
+ env: {
1229
+ ...process.env,
1230
+ HOME: workDir,
1231
+ PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"
1232
+ },
1233
+ maxBuffer: 10 * 1024 * 1024
1234
+ // 10MB
1235
+ });
1236
+ return { stdout, stderr };
1237
+ },
1238
+ async grep(pattern) {
1239
+ const { stdout } = await runExec(
1240
+ `grep -rn --include='*' "${pattern.replace(/"/g, '\\"')}" . 2>/dev/null || true`,
1241
+ { cwd: workDir, timeout: 1e4 }
1242
+ );
1243
+ return stdout;
1244
+ },
1245
+ async destroy() {
1246
+ await rm(workDir, { recursive: true, force: true });
1247
+ }
1248
+ };
1249
+ try {
1250
+ return await next(req, ctx);
1251
+ } finally {
1252
+ }
1253
+ };
1254
+ }
1255
+
1116
1256
  // src/middleware/esbuild-dev.ts
1117
- import { resolve as resolve2, relative, dirname } from "node:path";
1118
- import { stat, readFile, realpath as realpath2 } from "node:fs/promises";
1257
+ import { resolve as resolve3, relative, dirname } from "node:path";
1258
+ import { stat, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
1119
1259
  function escapeHtml(s) {
1120
1260
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1121
1261
  }
@@ -1160,14 +1300,14 @@ async function collectDeps(entryPath) {
1160
1300
  continue;
1161
1301
  }
1162
1302
  try {
1163
- const content = await readFile(p, "utf-8");
1303
+ const content = await readFile2(p, "utf-8");
1164
1304
  const importRe = /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g;
1165
1305
  let m;
1166
1306
  while ((m = importRe.exec(content)) !== null) {
1167
1307
  const spec = m[1] ?? m[2];
1168
1308
  if (spec.startsWith(".") || spec.startsWith("/")) {
1169
1309
  const dir = dirname(p);
1170
- const resolved = resolve2(dir, spec);
1310
+ const resolved = resolve3(dir, spec);
1171
1311
  for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", "/index.ts", "/index.tsx", "/index.js"]) {
1172
1312
  const candidate = resolved + ext;
1173
1313
  if (!visited.has(candidate)) {
@@ -1274,7 +1414,7 @@ ${entries2.join("\n")}
1274
1414
  format: entry.format ?? "esm",
1275
1415
  sourcemap: entry.sourcemap ?? false,
1276
1416
  splitting: entry.splitting ?? false,
1277
- ...entry.splitting ? { outdir: resolve2(".weifuwu-esbuild-out") } : {},
1417
+ ...entry.splitting ? { outdir: resolve3(".weifuwu-esbuild-out") } : {},
1278
1418
  define: entry.define,
1279
1419
  loader: entry.loader,
1280
1420
  write: false,
@@ -1295,7 +1435,7 @@ ${entries2.join("\n")}
1295
1435
  }]
1296
1436
  };
1297
1437
  } else {
1298
- entryAbs = resolve2(entry.entry);
1438
+ entryAbs = resolve3(entry.entry);
1299
1439
  buildOpts = {
1300
1440
  entryPoints: [entryAbs],
1301
1441
  bundle: entry.bundle ?? true,
@@ -1412,7 +1552,7 @@ ${entries2.join("\n")}
1412
1552
  }
1413
1553
  const { code, etag, chunks } = await compile(config);
1414
1554
  const depEntry = config.clientRouter?.routes ?? (config.clientRouter?.pages ? Object.values(config.clientRouter.pages)[0] : void 0) ?? config.entry;
1415
- const deps = await collectDeps(resolve2(depEntry));
1555
+ const deps = await collectDeps(resolve3(depEntry));
1416
1556
  if (cache === "memory") {
1417
1557
  cacheStore.set(pathname, { code, etag, files: deps, chunks });
1418
1558
  if (chunks) {
@@ -1447,8 +1587,8 @@ function hashCode(s) {
1447
1587
  }
1448
1588
 
1449
1589
  // src/middleware/tailwind-dev.ts
1450
- import { resolve as resolve3 } from "node:path";
1451
- import { stat as stat2, readFile as readFile2, glob } from "node:fs/promises";
1590
+ import { resolve as resolve4 } from "node:path";
1591
+ import { stat as stat2, readFile as readFile3, glob } from "node:fs/promises";
1452
1592
  function escapeHtml2(s) {
1453
1593
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1454
1594
  }
@@ -1482,11 +1622,11 @@ var CLASS_CONTEXT_RE = /(?:class(?:Name)?\s*[=:]\s*["'`]([^"'`]+)["'`]|["'`]([^"
1482
1622
  async function extractCandidates(patterns, root) {
1483
1623
  const seen = /* @__PURE__ */ new Set();
1484
1624
  for (const pattern of patterns) {
1485
- const resolved = resolve3(root, pattern);
1625
+ const resolved = resolve4(root, pattern);
1486
1626
  try {
1487
1627
  for await (const file of glob(resolved)) {
1488
1628
  try {
1489
- const content = await readFile2(file, "utf-8");
1629
+ const content = await readFile3(file, "utf-8");
1490
1630
  for (const m of content.matchAll(CLASS_CONTEXT_RE)) {
1491
1631
  const str = m[1] ?? m[2];
1492
1632
  if (!str) continue;
@@ -1553,9 +1693,9 @@ ${String(err)}`;
1553
1693
  }
1554
1694
  async function compileCss(entry) {
1555
1695
  const compile = await getCompile();
1556
- const entryAbs = resolve3(entry.entry);
1557
- const cssSource = await readFile2(entryAbs, "utf-8");
1558
- const baseDir = resolve3(entry.entry, "..");
1696
+ const entryAbs = resolve4(entry.entry);
1697
+ const cssSource = await readFile3(entryAbs, "utf-8");
1698
+ const baseDir = resolve4(entry.entry, "..");
1559
1699
  const deps = [entryAbs];
1560
1700
  const result = await compile(cssSource, {
1561
1701
  base: baseDir,
@@ -1566,7 +1706,7 @@ ${String(err)}`;
1566
1706
  const contentPatterns = entry.content ?? [];
1567
1707
  for (const src of result.sources) {
1568
1708
  if (!src.negated) {
1569
- contentPatterns.push(resolve3(src.base, src.pattern));
1709
+ contentPatterns.push(resolve4(src.base, src.pattern));
1570
1710
  }
1571
1711
  }
1572
1712
  let candidates = [];
@@ -1576,7 +1716,7 @@ ${String(err)}`;
1576
1716
  const css = result.build(candidates);
1577
1717
  for (const pattern of contentPatterns) {
1578
1718
  try {
1579
- for await (const file of glob(resolve3(baseDir, pattern))) {
1719
+ for await (const file of glob(resolve4(baseDir, pattern))) {
1580
1720
  if (!deps.includes(file)) deps.push(file);
1581
1721
  }
1582
1722
  } catch {
@@ -1746,10 +1886,10 @@ function addRateLimitHeaders(res, limit, remaining, reset) {
1746
1886
 
1747
1887
  // src/middleware/compress.ts
1748
1888
  import { constants, brotliCompress, gzip, deflate } from "node:zlib";
1749
- import { promisify } from "node:util";
1750
- var brotliCompressAsync = promisify(brotliCompress);
1751
- var gzipAsync = promisify(gzip);
1752
- var deflateAsync = promisify(deflate);
1889
+ import { promisify as promisify2 } from "node:util";
1890
+ var brotliCompressAsync = promisify2(brotliCompress);
1891
+ var gzipAsync = promisify2(gzip);
1892
+ var deflateAsync = promisify2(deflate);
1753
1893
  function compress(options) {
1754
1894
  const level = options?.level ?? 6;
1755
1895
  const threshold = options?.threshold ?? 1024;
@@ -2154,7 +2294,7 @@ function redis(opts) {
2154
2294
  }
2155
2295
 
2156
2296
  // src/queue/index.ts
2157
- import crypto2 from "node:crypto";
2297
+ import crypto3 from "node:crypto";
2158
2298
  import { Redis as IORedis2 } from "ioredis";
2159
2299
 
2160
2300
  // src/queue/cron.ts
@@ -2268,7 +2408,7 @@ function queue(opts) {
2268
2408
  }
2269
2409
  if (job.schedule) {
2270
2410
  try {
2271
- await insert({ ...job, id: crypto2.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
2411
+ await insert({ ...job, id: crypto3.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
2272
2412
  } catch {
2273
2413
  }
2274
2414
  }
@@ -2294,7 +2434,7 @@ function queue(opts) {
2294
2434
  });
2295
2435
  const q = mw;
2296
2436
  mw.add = function add(type, payload, opts2) {
2297
- const id = crypto2.randomUUID();
2437
+ const id = crypto3.randomUUID();
2298
2438
  let runAt;
2299
2439
  if (opts2?.schedule) {
2300
2440
  try {
@@ -2412,7 +2552,7 @@ function queue(opts) {
2412
2552
  return r;
2413
2553
  };
2414
2554
  q.cron = function(pattern, handler) {
2415
- const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto2.randomUUID().slice(0, 8);
2555
+ const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto3.randomUUID().slice(0, 8);
2416
2556
  q.process(id, async () => {
2417
2557
  await handler();
2418
2558
  });
@@ -2427,8 +2567,8 @@ import { createElement as createElement2 } from "react";
2427
2567
  import { renderToReadableStream } from "react-dom/server";
2428
2568
 
2429
2569
  // src/react/compile.ts
2430
- import { mkdir as mkdir2, writeFile as writeFile2, readFile as readFile3 } from "node:fs/promises";
2431
- import { resolve as resolve4, join as join2, dirname as dirname2 } from "node:path";
2570
+ import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4 } from "node:fs/promises";
2571
+ import { resolve as resolve5, join as join3, dirname as dirname2 } from "node:path";
2432
2572
  import { createHash } from "node:crypto";
2433
2573
  import { existsSync } from "node:fs";
2434
2574
  var EXTERNAL_PKGS = [
@@ -2481,14 +2621,14 @@ function setReactCacheDir(dir) {
2481
2621
  }
2482
2622
  function getCacheDir() {
2483
2623
  if (_cacheDir) return _cacheDir;
2484
- _cacheDir = join2(process.cwd(), "node_modules", ".weifuwu", "react");
2624
+ _cacheDir = join3(process.cwd(), "node_modules", ".weifuwu", "react");
2485
2625
  return _cacheDir;
2486
2626
  }
2487
2627
  async function loadTsxModule(entryPath) {
2488
- const abs = resolve4(entryPath);
2628
+ const abs = resolve5(entryPath);
2489
2629
  let source;
2490
2630
  try {
2491
- source = await readFile3(abs, "utf-8");
2631
+ source = await readFile4(abs, "utf-8");
2492
2632
  } catch {
2493
2633
  throw new Error(`Cannot read file: ${entryPath}`);
2494
2634
  }
@@ -2498,7 +2638,7 @@ async function loadTsxModule(entryPath) {
2498
2638
  return memCached.mod;
2499
2639
  }
2500
2640
  const tmpDir = getCacheDir();
2501
- const tmpFile = join2(tmpDir, `${sourceHash}.mjs`);
2641
+ const tmpFile = join3(tmpDir, `${sourceHash}.mjs`);
2502
2642
  if (existsSync(tmpFile)) {
2503
2643
  try {
2504
2644
  const mod2 = await import(tmpFile + "?" + sourceHash);
@@ -2521,8 +2661,8 @@ async function loadTsxModule(entryPath) {
2521
2661
  let code = result.outputFiles[0]?.text ?? "";
2522
2662
  if (!code) throw new Error(`esbuild produced empty output for ${entryPath}`);
2523
2663
  code = rewriteImports(code);
2524
- await mkdir2(dirname2(tmpFile), { recursive: true });
2525
- await writeFile2(tmpFile, code);
2664
+ await mkdir3(dirname2(tmpFile), { recursive: true });
2665
+ await writeFile3(tmpFile, code);
2526
2666
  const mod = await import(tmpFile + "?" + sourceHash);
2527
2667
  memCache.set(abs, { sourceHash, mod });
2528
2668
  return mod;
@@ -2794,6 +2934,149 @@ function reactRouter(app, routes, opts = {}) {
2794
2934
  function Link({ href, children, ...props }) {
2795
2935
  return createElement2("a", { href, ...props }, children);
2796
2936
  }
2937
+
2938
+ // src/ai/index.ts
2939
+ function ai(opts) {
2940
+ let aiModule = null;
2941
+ const model = opts.model;
2942
+ async function getAi() {
2943
+ if (!aiModule) aiModule = await import("ai");
2944
+ return aiModule;
2945
+ }
2946
+ return async (_req, ctx, next) => {
2947
+ ctx.ai = {
2948
+ model,
2949
+ system: opts.system,
2950
+ async generateText(params) {
2951
+ const { generateText } = await getAi();
2952
+ const messages = [...params.messages ?? []];
2953
+ const system = params.system ?? opts.system;
2954
+ if (system) {
2955
+ const hasSystem = messages.some((m) => m.role === "system");
2956
+ if (!hasSystem) {
2957
+ messages.unshift({ role: "system", content: system });
2958
+ }
2959
+ }
2960
+ const baseOpts = {
2961
+ model,
2962
+ tools: opts.tools,
2963
+ maxSteps: params.maxSteps ?? opts.maxSteps ?? 1,
2964
+ temperature: params.temperature ?? opts.temperature,
2965
+ maxTokens: params.maxTokens ?? opts.maxTokens,
2966
+ abortSignal: _req.signal
2967
+ };
2968
+ if (messages.length > 0) {
2969
+ baseOpts.messages = messages;
2970
+ } else {
2971
+ baseOpts.prompt = params.prompt;
2972
+ }
2973
+ return generateText(baseOpts);
2974
+ }
2975
+ };
2976
+ return next(_req, ctx);
2977
+ };
2978
+ }
2979
+
2980
+ // src/ai/agent.ts
2981
+ function agent(opts) {
2982
+ let aiModule = null;
2983
+ async function getAi() {
2984
+ if (!aiModule) aiModule = await import("ai");
2985
+ return aiModule;
2986
+ }
2987
+ return async (req, ctx, next) => {
2988
+ ctx.agent = {
2989
+ model: opts.model,
2990
+ system: opts.system,
2991
+ async chat(prompt, chatOpts = {}) {
2992
+ const { generateText } = await getAi();
2993
+ let knowledgeContext = "";
2994
+ if (opts.knowledge) {
2995
+ try {
2996
+ const results = await opts.knowledge.search(prompt, ctx);
2997
+ const topK = opts.knowledge.topK ?? 3;
2998
+ const minScore = opts.knowledge.minScore ?? 0;
2999
+ const filtered = results.filter((r) => (r.score ?? 1) >= minScore).slice(0, topK);
3000
+ if (filtered.length > 0) {
3001
+ knowledgeContext = "\n\n\u53C2\u8003\u4EE5\u4E0B\u77E5\u8BC6\uFF1A\n" + filtered.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
3002
+ }
3003
+ } catch {
3004
+ }
3005
+ }
3006
+ const system = chatOpts.system ?? opts.system ?? "";
3007
+ const messages = [...chatOpts.messages ?? []];
3008
+ if (system) {
3009
+ const hasSystem = messages.some((m) => m.role === "system");
3010
+ if (!hasSystem) {
3011
+ messages.unshift({
3012
+ role: "system",
3013
+ content: knowledgeContext ? system + knowledgeContext : system
3014
+ });
3015
+ } else if (knowledgeContext) {
3016
+ const sysMsg = messages.find((m) => m.role === "system");
3017
+ if (sysMsg) sysMsg.content += knowledgeContext;
3018
+ }
3019
+ }
3020
+ if (prompt && !messages.some((m) => m.role === "user")) {
3021
+ messages.push({ role: "user", content: prompt });
3022
+ }
3023
+ const result = await generateText({
3024
+ model: opts.model,
3025
+ messages,
3026
+ tools: opts.tools,
3027
+ maxSteps: chatOpts.maxSteps ?? opts.maxSteps ?? 1,
3028
+ temperature: chatOpts.temperature ?? opts.temperature,
3029
+ maxTokens: chatOpts.maxTokens ?? opts.maxTokens,
3030
+ abortSignal: req.signal
3031
+ });
3032
+ return {
3033
+ text: result.text,
3034
+ toolCalls: result.toolCalls,
3035
+ toolResults: result.toolResults,
3036
+ steps: result.steps,
3037
+ finishReason: result.finishReason,
3038
+ usage: result.usage
3039
+ };
3040
+ },
3041
+ chatStreamResponse(streamOpts) {
3042
+ const encoder = new TextEncoder();
3043
+ const stream = new ReadableStream({
3044
+ async start(controller) {
3045
+ try {
3046
+ const result = await ctx.agent.chat(
3047
+ streamOpts.messages.filter((m) => m.role === "user").pop()?.content ?? "",
3048
+ {
3049
+ messages: streamOpts.messages,
3050
+ system: streamOpts.system
3051
+ }
3052
+ );
3053
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "text", content: result.text })}
3054
+
3055
+ `));
3056
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
3057
+ controller.close();
3058
+ } catch (err) {
3059
+ controller.enqueue(encoder.encode(
3060
+ `data: ${JSON.stringify({ type: "error", error: String(err) })}
3061
+
3062
+ `
3063
+ ));
3064
+ controller.close();
3065
+ }
3066
+ }
3067
+ });
3068
+ return new Response(stream, {
3069
+ headers: {
3070
+ "Content-Type": "text/event-stream",
3071
+ "Cache-Control": "no-cache",
3072
+ Connection: "keep-alive"
3073
+ }
3074
+ });
3075
+ }
3076
+ };
3077
+ return next(req, ctx);
3078
+ };
3079
+ }
2797
3080
  export {
2798
3081
  DEFAULT_MAX_BODY,
2799
3082
  ErrorBoundary,
@@ -2802,6 +3085,9 @@ export {
2802
3085
  MIGRATIONS_TABLE,
2803
3086
  Router,
2804
3087
  ServerDataContext,
3088
+ agent,
3089
+ ai,
3090
+ auth,
2805
3091
  compress,
2806
3092
  cors,
2807
3093
  createHub,
@@ -2818,6 +3104,7 @@ export {
2818
3104
  reactRouter,
2819
3105
  redis,
2820
3106
  runWithTrace,
3107
+ sandbox,
2821
3108
  serve,
2822
3109
  serveStatic,
2823
3110
  tailwindDev,
@@ -0,0 +1,68 @@
1
+ import type { Middleware, User } from '../types.ts';
2
+ export interface AuthOptions {
3
+ /**
4
+ * JWT authentication.
5
+ *
6
+ * @example
7
+ * auth({ jwt: { secret: process.env.JWT_SECRET } })
8
+ */
9
+ jwt?: {
10
+ /** HMAC secret for HS256. */
11
+ secret: string;
12
+ /** Cookie name for the token (default: 'token'). */
13
+ cookie?: string;
14
+ };
15
+ /**
16
+ * Simple session authentication via signed cookie.
17
+ *
18
+ * @example
19
+ * auth({ session: { secret: '...' } })
20
+ */
21
+ session?: {
22
+ /** Secret for HMAC cookie signing. */
23
+ secret: string;
24
+ /** Cookie name (default: 'session'). */
25
+ cookie?: string;
26
+ /** Load user from session data. */
27
+ loadUser: (data: Record<string, unknown>) => Promise<User | null> | User | null;
28
+ };
29
+ /**
30
+ * API key authentication via header.
31
+ *
32
+ * @example
33
+ * auth({ apiKey: { header: 'x-api-key', keys: ['sk-xxx'] } })
34
+ */
35
+ apiKey?: {
36
+ /** Header name (default: 'authorization'). */
37
+ header?: string;
38
+ /** Prefix to strip (default: 'Bearer '). */
39
+ prefix?: string;
40
+ /** Validate an API key → User or null. */
41
+ validate: (key: string) => Promise<User | null> | User | null;
42
+ };
43
+ }
44
+ /**
45
+ * Authentication middleware — injects `ctx.user`.
46
+ *
47
+ * Supports JWT (HS256), signed session cookies, and API keys.
48
+ * When no user is authenticated, requests still proceed —
49
+ * check `ctx.user` in your handlers to enforce auth.
50
+ *
51
+ * @example
52
+ * ```ts
53
+ * // JWT
54
+ * app.use(auth({ jwt: { secret: 'my-secret' } }))
55
+ *
56
+ * // Session cookie
57
+ * app.use(auth({
58
+ * session: {
59
+ * secret: '...',
60
+ * loadUser: async (data) => db.findUser(data.userId),
61
+ * },
62
+ * }))
63
+ *
64
+ * // API key
65
+ * app.use(auth({ apiKey: { validate: async (key) => db.findByApiKey(key) } }))
66
+ * ```
67
+ */
68
+ export declare function auth(opts: AuthOptions): Middleware;
@@ -0,0 +1,52 @@
1
+ import type { Middleware } from '../types.ts';
2
+ declare module '../types.ts' {
3
+ interface Context {
4
+ /** Filesystem sandbox for agent operations. Injected by sandbox(). */
5
+ sandbox: Sandbox;
6
+ }
7
+ }
8
+ export interface SandboxOptions {
9
+ /** Root directory for all workspaces (default: '/tmp/weifuwu-sandboxes'). */
10
+ baseDir?: string;
11
+ /** Default timeout for exec() in ms (default: 30000). */
12
+ timeout?: number;
13
+ /** Isolate workspaces by a ctx field. If 'user', uses ctx.user.id. */
14
+ isolateBy?: 'user';
15
+ }
16
+ export interface ExecResult {
17
+ stdout: string;
18
+ stderr: string;
19
+ }
20
+ export interface Sandbox {
21
+ /** The workspace directory for the current request. */
22
+ workDir: string;
23
+ /** Read a file inside the workspace. */
24
+ readFile(path: string): Promise<string>;
25
+ /** Write a file inside the workspace. Creates parent directories. */
26
+ writeFile(path: string, content: string): Promise<void>;
27
+ /** Execute a command inside the workspace. */
28
+ exec(cmd: string, opts?: {
29
+ timeout?: number;
30
+ cwd?: string;
31
+ }): Promise<ExecResult>;
32
+ /** Grep for a pattern inside the workspace. */
33
+ grep(pattern: string): Promise<string>;
34
+ /** Destroy the workspace (called when the session ends). */
35
+ destroy(): Promise<void>;
36
+ }
37
+ /**
38
+ * Filesystem sandbox middleware.
39
+ *
40
+ * Each request gets an isolated workspace directory. File and exec
41
+ * operations are restricted to the workspace — path escapes are rejected.
42
+ *
43
+ * @example
44
+ * ```ts
45
+ * app.use(sandbox({ baseDir: '/tmp/workspaces', isolateBy: 'user' }))
46
+ *
47
+ * // In a handler:
48
+ * await ctx.sandbox.writeFile('hello.txt', 'world')
49
+ * const content = await ctx.sandbox.readFile('hello.txt')
50
+ * ```
51
+ */
52
+ export declare function sandbox(opts?: SandboxOptions): Middleware;
package/dist/types.d.ts CHANGED
@@ -19,6 +19,13 @@ export interface WebSocket {
19
19
  removeEventListener(event: string, handler: (...args: unknown[]) => void): void;
20
20
  }
21
21
  export type { Redis, RedisOptions } from 'ioredis';
22
+ /** User injected by auth() middleware. */
23
+ export interface User {
24
+ id: string;
25
+ role?: string;
26
+ tenant?: string;
27
+ [key: string]: unknown;
28
+ }
22
29
  export interface WsContext {
23
30
  /** Per-connection state object */
24
31
  state: Record<string, unknown>;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.31.2",
4
+ "version": "0.32.0",
5
5
  "description": "Web-standard HTTP microframework for Node.js — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {
@@ -32,6 +32,7 @@
32
32
  },
33
33
  "dependencies": {
34
34
  "@graphql-tools/schema": "^10",
35
+ "ai": "^7.0.14",
35
36
  "graphql": "^16",
36
37
  "ioredis": "^5.11.0",
37
38
  "postgres": "^3.4.9",