weifuwu 0.31.2 → 0.32.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
@@ -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,113 @@ 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
+ JWT extraction priority: `cookie` → `Authorization: Bearer` → `?access_token=`. All callbacks receive `ctx` so `ctx.sql` is directly available.
374
+
375
+ ```ts
376
+ import { auth } from 'weifuwu'
377
+
378
+ // JWT — cookie, header, or ?access_token=
379
+ app.use(auth({ jwt: { secret: process.env.JWT_SECRET } }))
380
+
381
+ // Session cookie — loadUser gets ctx
382
+ app.use(auth({
383
+ session: {
384
+ secret: '...',
385
+ loadUser: async (data, ctx) => ctx.sql`SELECT * FROM users WHERE id = ${data.userId}`,
386
+ },
387
+ }))
388
+
389
+ // API key — header or ?api_key=
390
+ app.use(auth({ apiKey: {
391
+ query: 'api_key',
392
+ validate: async (key, ctx) => ctx.sql`SELECT * FROM users WHERE api_key = ${key}`,
393
+ } }))
394
+
395
+ // ctx.user is now available
396
+ app.get('/me', (req, ctx) => {
397
+ if (!ctx.user) return new Response('Unauthorized', { status: 401 })
398
+ return Response.json(ctx.user)
399
+ })
400
+ ```
401
+
402
+ 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.
403
+
404
+ ### Sandbox
405
+
406
+ ```ts
407
+ import { sandbox } from 'weifuwu'
408
+
409
+ app.use(sandbox({
410
+ baseDir: '/tmp/workspaces',
411
+ timeout: 30000,
412
+ isolateBy: 'user', // one directory per ctx.user.id
413
+ }))
414
+
415
+ // ctx.sandbox provides isolated file + exec operations
416
+ await ctx.sandbox.writeFile('hello.txt', 'world')
417
+ const content = await ctx.sandbox.readFile('hello.txt')
418
+ const { stdout } = await ctx.sandbox.exec('ls -la')
419
+ await ctx.sandbox.destroy() // clean up workspace
420
+ ```
421
+
422
+ 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`.
423
+
316
424
  ### Types
317
425
 
318
426
  | Export | Description |
@@ -368,50 +476,35 @@ app.onError((err, req, ctx) => {
368
476
  ## Complete example
369
477
 
370
478
  ```ts
371
- import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, queue, HttpError } from 'weifuwu'
479
+ import { serve, Router, cors, helmet, compress, logger, trace, rateLimit, postgres, redis, auth, sandbox, HttpError } from 'weifuwu'
480
+ import { agent } from 'weifuwu/agent'
481
+ import { react } from 'weifuwu/react'
482
+ import { openai } from '@ai-sdk/openai'
372
483
 
373
484
  const app = new Router()
485
+ .use(trace())
486
+ .use(logger())
487
+ .use(cors())
488
+ .use(helmet())
489
+ .use(compress())
490
+ .use(postgres())
491
+ .use(redis())
492
+ .use(auth({ jwt: { secret: process.env.JWT_SECRET! } }))
493
+ .use(sandbox({ isolateBy: 'user' }))
494
+ .use(agent({
495
+ model: openai('gpt-4o'),
496
+ system: 'You are a helpful assistant.',
497
+ knowledge: {
498
+ search: async (q, ctx) => ctx.sql`...`,
499
+ },
500
+ sandbox: true,
501
+ }))
502
+ .plugin(react({ pages: { '/': './Chat.tsx' }, layout: './Layout.tsx', tailwind: {} }))
374
503
 
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 })
504
+ app.post('/api/chat', async (req, ctx) => {
505
+ if (!ctx.user) return new Response('Unauthorized', { status: 401 })
506
+ const { messages } = await req.json()
507
+ return ctx.agent.chatStreamResponse({ messages })
415
508
  })
416
509
 
417
510
  serve(app, { port: 3000 })
@@ -432,6 +525,7 @@ weifuwu/
432
525
  │ ├── types.ts
433
526
  │ ├── core/ ← serve, router, ws, trace, logger
434
527
  │ ├── middleware/ ← cors, helmet, compress, rate-limit, static, upload
528
+ │ ├── ai/ ← AI + agent middleware
435
529
  │ ├── postgres/
436
530
  │ ├── redis/
437
531
  │ ├── 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,82 @@ 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, "") ?? ctx.query.access_token;
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, ctx);
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
+ let raw = req.headers.get(header);
971
+ if (!raw && opts.apiKey.query) {
972
+ raw = ctx.query[opts.apiKey.query];
973
+ if (raw) raw = prefix + raw;
974
+ }
975
+ if (raw?.startsWith(prefix)) {
976
+ const key = raw.slice(prefix.length);
977
+ const user = await opts.apiKey.validate(key, ctx);
978
+ if (user) ctx.user = user;
979
+ }
980
+ }
981
+ return next(req, ctx);
982
+ };
983
+ }
984
+
909
985
  // src/middleware/static.ts
910
986
  import { open, realpath } from "node:fs/promises";
911
987
  import { extname, resolve, normalize, sep } from "node:path";
@@ -1113,9 +1189,77 @@ function upload(options) {
1113
1189
  return mw;
1114
1190
  }
1115
1191
 
1192
+ // src/middleware/sandbox.ts
1193
+ import { join as join2, resolve as resolve2, normalize as normalize2 } from "node:path";
1194
+ import { mkdir as mkdir2, readFile, writeFile as writeFile2, rm } from "node:fs/promises";
1195
+ import { exec as cpExec } from "node:child_process";
1196
+ import { promisify } from "node:util";
1197
+ var runExec = promisify(cpExec);
1198
+ function sandbox(opts = {}) {
1199
+ const base = resolve2(opts.baseDir ?? "/tmp/weifuwu-sandboxes");
1200
+ const defaultTimeout = opts.timeout ?? 3e4;
1201
+ return async (req, ctx, next) => {
1202
+ let sessionId;
1203
+ if (opts.isolateBy === "user" && ctx.user) {
1204
+ sessionId = ctx.user.id ?? "default";
1205
+ } else {
1206
+ sessionId = crypto.randomUUID();
1207
+ }
1208
+ const workDir = join2(base, encodeURIComponent(sessionId));
1209
+ await mkdir2(workDir, { recursive: true });
1210
+ function safePath(p) {
1211
+ const resolved = resolve2(workDir, normalize2(p).replace(/^[/\\]+/, ""));
1212
+ if (!resolved.startsWith(workDir + "/") && resolved !== workDir) {
1213
+ throw new Error(`Path escape rejected: ${p}`);
1214
+ }
1215
+ return resolved;
1216
+ }
1217
+ ctx.sandbox = {
1218
+ workDir,
1219
+ async readFile(path) {
1220
+ return readFile(safePath(path), "utf-8");
1221
+ },
1222
+ async writeFile(path, content) {
1223
+ const resolved = safePath(path);
1224
+ await mkdir2(join2(resolved, ".."), { recursive: true });
1225
+ return writeFile2(resolved, content, "utf-8");
1226
+ },
1227
+ async exec(cmd, execOpts) {
1228
+ const cwd = execOpts?.cwd ? safePath(execOpts.cwd) : workDir;
1229
+ const { stdout, stderr } = await runExec(cmd, {
1230
+ cwd,
1231
+ timeout: execOpts?.timeout ?? defaultTimeout,
1232
+ env: {
1233
+ ...process.env,
1234
+ HOME: workDir,
1235
+ PATH: process.env.PATH ?? "/usr/local/bin:/usr/bin:/bin"
1236
+ },
1237
+ maxBuffer: 10 * 1024 * 1024
1238
+ // 10MB
1239
+ });
1240
+ return { stdout, stderr };
1241
+ },
1242
+ async grep(pattern) {
1243
+ const { stdout } = await runExec(
1244
+ `grep -rn --include='*' "${pattern.replace(/"/g, '\\"')}" . 2>/dev/null || true`,
1245
+ { cwd: workDir, timeout: 1e4 }
1246
+ );
1247
+ return stdout;
1248
+ },
1249
+ async destroy() {
1250
+ await rm(workDir, { recursive: true, force: true });
1251
+ }
1252
+ };
1253
+ try {
1254
+ return await next(req, ctx);
1255
+ } finally {
1256
+ }
1257
+ };
1258
+ }
1259
+
1116
1260
  // 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";
1261
+ import { resolve as resolve3, relative, dirname } from "node:path";
1262
+ import { stat, readFile as readFile2, realpath as realpath2 } from "node:fs/promises";
1119
1263
  function escapeHtml(s) {
1120
1264
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1121
1265
  }
@@ -1160,14 +1304,14 @@ async function collectDeps(entryPath) {
1160
1304
  continue;
1161
1305
  }
1162
1306
  try {
1163
- const content = await readFile(p, "utf-8");
1307
+ const content = await readFile2(p, "utf-8");
1164
1308
  const importRe = /from\s+['"]([^'"]+)['"]|import\s+['"]([^'"]+)['"]/g;
1165
1309
  let m;
1166
1310
  while ((m = importRe.exec(content)) !== null) {
1167
1311
  const spec = m[1] ?? m[2];
1168
1312
  if (spec.startsWith(".") || spec.startsWith("/")) {
1169
1313
  const dir = dirname(p);
1170
- const resolved = resolve2(dir, spec);
1314
+ const resolved = resolve3(dir, spec);
1171
1315
  for (const ext of ["", ".ts", ".tsx", ".js", ".jsx", ".mjs", "/index.ts", "/index.tsx", "/index.js"]) {
1172
1316
  const candidate = resolved + ext;
1173
1317
  if (!visited.has(candidate)) {
@@ -1274,7 +1418,7 @@ ${entries2.join("\n")}
1274
1418
  format: entry.format ?? "esm",
1275
1419
  sourcemap: entry.sourcemap ?? false,
1276
1420
  splitting: entry.splitting ?? false,
1277
- ...entry.splitting ? { outdir: resolve2(".weifuwu-esbuild-out") } : {},
1421
+ ...entry.splitting ? { outdir: resolve3(".weifuwu-esbuild-out") } : {},
1278
1422
  define: entry.define,
1279
1423
  loader: entry.loader,
1280
1424
  write: false,
@@ -1295,7 +1439,7 @@ ${entries2.join("\n")}
1295
1439
  }]
1296
1440
  };
1297
1441
  } else {
1298
- entryAbs = resolve2(entry.entry);
1442
+ entryAbs = resolve3(entry.entry);
1299
1443
  buildOpts = {
1300
1444
  entryPoints: [entryAbs],
1301
1445
  bundle: entry.bundle ?? true,
@@ -1412,7 +1556,7 @@ ${entries2.join("\n")}
1412
1556
  }
1413
1557
  const { code, etag, chunks } = await compile(config);
1414
1558
  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));
1559
+ const deps = await collectDeps(resolve3(depEntry));
1416
1560
  if (cache === "memory") {
1417
1561
  cacheStore.set(pathname, { code, etag, files: deps, chunks });
1418
1562
  if (chunks) {
@@ -1447,8 +1591,8 @@ function hashCode(s) {
1447
1591
  }
1448
1592
 
1449
1593
  // 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";
1594
+ import { resolve as resolve4 } from "node:path";
1595
+ import { stat as stat2, readFile as readFile3, glob } from "node:fs/promises";
1452
1596
  function escapeHtml2(s) {
1453
1597
  return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1454
1598
  }
@@ -1482,11 +1626,11 @@ var CLASS_CONTEXT_RE = /(?:class(?:Name)?\s*[=:]\s*["'`]([^"'`]+)["'`]|["'`]([^"
1482
1626
  async function extractCandidates(patterns, root) {
1483
1627
  const seen = /* @__PURE__ */ new Set();
1484
1628
  for (const pattern of patterns) {
1485
- const resolved = resolve3(root, pattern);
1629
+ const resolved = resolve4(root, pattern);
1486
1630
  try {
1487
1631
  for await (const file of glob(resolved)) {
1488
1632
  try {
1489
- const content = await readFile2(file, "utf-8");
1633
+ const content = await readFile3(file, "utf-8");
1490
1634
  for (const m of content.matchAll(CLASS_CONTEXT_RE)) {
1491
1635
  const str = m[1] ?? m[2];
1492
1636
  if (!str) continue;
@@ -1553,9 +1697,9 @@ ${String(err)}`;
1553
1697
  }
1554
1698
  async function compileCss(entry) {
1555
1699
  const compile = await getCompile();
1556
- const entryAbs = resolve3(entry.entry);
1557
- const cssSource = await readFile2(entryAbs, "utf-8");
1558
- const baseDir = resolve3(entry.entry, "..");
1700
+ const entryAbs = resolve4(entry.entry);
1701
+ const cssSource = await readFile3(entryAbs, "utf-8");
1702
+ const baseDir = resolve4(entry.entry, "..");
1559
1703
  const deps = [entryAbs];
1560
1704
  const result = await compile(cssSource, {
1561
1705
  base: baseDir,
@@ -1566,7 +1710,7 @@ ${String(err)}`;
1566
1710
  const contentPatterns = entry.content ?? [];
1567
1711
  for (const src of result.sources) {
1568
1712
  if (!src.negated) {
1569
- contentPatterns.push(resolve3(src.base, src.pattern));
1713
+ contentPatterns.push(resolve4(src.base, src.pattern));
1570
1714
  }
1571
1715
  }
1572
1716
  let candidates = [];
@@ -1576,7 +1720,7 @@ ${String(err)}`;
1576
1720
  const css = result.build(candidates);
1577
1721
  for (const pattern of contentPatterns) {
1578
1722
  try {
1579
- for await (const file of glob(resolve3(baseDir, pattern))) {
1723
+ for await (const file of glob(resolve4(baseDir, pattern))) {
1580
1724
  if (!deps.includes(file)) deps.push(file);
1581
1725
  }
1582
1726
  } catch {
@@ -1746,10 +1890,10 @@ function addRateLimitHeaders(res, limit, remaining, reset) {
1746
1890
 
1747
1891
  // src/middleware/compress.ts
1748
1892
  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);
1893
+ import { promisify as promisify2 } from "node:util";
1894
+ var brotliCompressAsync = promisify2(brotliCompress);
1895
+ var gzipAsync = promisify2(gzip);
1896
+ var deflateAsync = promisify2(deflate);
1753
1897
  function compress(options) {
1754
1898
  const level = options?.level ?? 6;
1755
1899
  const threshold = options?.threshold ?? 1024;
@@ -2154,7 +2298,7 @@ function redis(opts) {
2154
2298
  }
2155
2299
 
2156
2300
  // src/queue/index.ts
2157
- import crypto2 from "node:crypto";
2301
+ import crypto3 from "node:crypto";
2158
2302
  import { Redis as IORedis2 } from "ioredis";
2159
2303
 
2160
2304
  // src/queue/cron.ts
@@ -2268,7 +2412,7 @@ function queue(opts) {
2268
2412
  }
2269
2413
  if (job.schedule) {
2270
2414
  try {
2271
- await insert({ ...job, id: crypto2.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
2415
+ await insert({ ...job, id: crypto3.randomUUID(), runAt: cronNext(job.schedule), createdAt: Date.now() });
2272
2416
  } catch {
2273
2417
  }
2274
2418
  }
@@ -2294,7 +2438,7 @@ function queue(opts) {
2294
2438
  });
2295
2439
  const q = mw;
2296
2440
  mw.add = function add(type, payload, opts2) {
2297
- const id = crypto2.randomUUID();
2441
+ const id = crypto3.randomUUID();
2298
2442
  let runAt;
2299
2443
  if (opts2?.schedule) {
2300
2444
  try {
@@ -2412,7 +2556,7 @@ function queue(opts) {
2412
2556
  return r;
2413
2557
  };
2414
2558
  q.cron = function(pattern, handler) {
2415
- const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto2.randomUUID().slice(0, 8);
2559
+ const id = "__cron_" + pattern.replace(/[^a-zA-Z0-9]/g, "_") + "_" + crypto3.randomUUID().slice(0, 8);
2416
2560
  q.process(id, async () => {
2417
2561
  await handler();
2418
2562
  });
@@ -2427,8 +2571,8 @@ import { createElement as createElement2 } from "react";
2427
2571
  import { renderToReadableStream } from "react-dom/server";
2428
2572
 
2429
2573
  // 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";
2574
+ import { mkdir as mkdir3, writeFile as writeFile3, readFile as readFile4 } from "node:fs/promises";
2575
+ import { resolve as resolve5, join as join3, dirname as dirname2 } from "node:path";
2432
2576
  import { createHash } from "node:crypto";
2433
2577
  import { existsSync } from "node:fs";
2434
2578
  var EXTERNAL_PKGS = [
@@ -2481,14 +2625,14 @@ function setReactCacheDir(dir) {
2481
2625
  }
2482
2626
  function getCacheDir() {
2483
2627
  if (_cacheDir) return _cacheDir;
2484
- _cacheDir = join2(process.cwd(), "node_modules", ".weifuwu", "react");
2628
+ _cacheDir = join3(process.cwd(), "node_modules", ".weifuwu", "react");
2485
2629
  return _cacheDir;
2486
2630
  }
2487
2631
  async function loadTsxModule(entryPath) {
2488
- const abs = resolve4(entryPath);
2632
+ const abs = resolve5(entryPath);
2489
2633
  let source;
2490
2634
  try {
2491
- source = await readFile3(abs, "utf-8");
2635
+ source = await readFile4(abs, "utf-8");
2492
2636
  } catch {
2493
2637
  throw new Error(`Cannot read file: ${entryPath}`);
2494
2638
  }
@@ -2498,7 +2642,7 @@ async function loadTsxModule(entryPath) {
2498
2642
  return memCached.mod;
2499
2643
  }
2500
2644
  const tmpDir = getCacheDir();
2501
- const tmpFile = join2(tmpDir, `${sourceHash}.mjs`);
2645
+ const tmpFile = join3(tmpDir, `${sourceHash}.mjs`);
2502
2646
  if (existsSync(tmpFile)) {
2503
2647
  try {
2504
2648
  const mod2 = await import(tmpFile + "?" + sourceHash);
@@ -2521,8 +2665,8 @@ async function loadTsxModule(entryPath) {
2521
2665
  let code = result.outputFiles[0]?.text ?? "";
2522
2666
  if (!code) throw new Error(`esbuild produced empty output for ${entryPath}`);
2523
2667
  code = rewriteImports(code);
2524
- await mkdir2(dirname2(tmpFile), { recursive: true });
2525
- await writeFile2(tmpFile, code);
2668
+ await mkdir3(dirname2(tmpFile), { recursive: true });
2669
+ await writeFile3(tmpFile, code);
2526
2670
  const mod = await import(tmpFile + "?" + sourceHash);
2527
2671
  memCache.set(abs, { sourceHash, mod });
2528
2672
  return mod;
@@ -2794,6 +2938,149 @@ function reactRouter(app, routes, opts = {}) {
2794
2938
  function Link({ href, children, ...props }) {
2795
2939
  return createElement2("a", { href, ...props }, children);
2796
2940
  }
2941
+
2942
+ // src/ai/index.ts
2943
+ function ai(opts) {
2944
+ let aiModule = null;
2945
+ const model = opts.model;
2946
+ async function getAi() {
2947
+ if (!aiModule) aiModule = await import("ai");
2948
+ return aiModule;
2949
+ }
2950
+ return async (_req, ctx, next) => {
2951
+ ctx.ai = {
2952
+ model,
2953
+ system: opts.system,
2954
+ async generateText(params) {
2955
+ const { generateText } = await getAi();
2956
+ const messages = [...params.messages ?? []];
2957
+ const system = params.system ?? opts.system;
2958
+ if (system) {
2959
+ const hasSystem = messages.some((m) => m.role === "system");
2960
+ if (!hasSystem) {
2961
+ messages.unshift({ role: "system", content: system });
2962
+ }
2963
+ }
2964
+ const baseOpts = {
2965
+ model,
2966
+ tools: opts.tools,
2967
+ maxSteps: params.maxSteps ?? opts.maxSteps ?? 1,
2968
+ temperature: params.temperature ?? opts.temperature,
2969
+ maxTokens: params.maxTokens ?? opts.maxTokens,
2970
+ abortSignal: _req.signal
2971
+ };
2972
+ if (messages.length > 0) {
2973
+ baseOpts.messages = messages;
2974
+ } else {
2975
+ baseOpts.prompt = params.prompt;
2976
+ }
2977
+ return generateText(baseOpts);
2978
+ }
2979
+ };
2980
+ return next(_req, ctx);
2981
+ };
2982
+ }
2983
+
2984
+ // src/ai/agent.ts
2985
+ function agent(opts) {
2986
+ let aiModule = null;
2987
+ async function getAi() {
2988
+ if (!aiModule) aiModule = await import("ai");
2989
+ return aiModule;
2990
+ }
2991
+ return async (req, ctx, next) => {
2992
+ ctx.agent = {
2993
+ model: opts.model,
2994
+ system: opts.system,
2995
+ async chat(prompt, chatOpts = {}) {
2996
+ const { generateText } = await getAi();
2997
+ let knowledgeContext = "";
2998
+ if (opts.knowledge) {
2999
+ try {
3000
+ const results = await opts.knowledge.search(prompt, ctx);
3001
+ const topK = opts.knowledge.topK ?? 3;
3002
+ const minScore = opts.knowledge.minScore ?? 0;
3003
+ const filtered = results.filter((r) => (r.score ?? 1) >= minScore).slice(0, topK);
3004
+ if (filtered.length > 0) {
3005
+ knowledgeContext = "\n\n\u53C2\u8003\u4EE5\u4E0B\u77E5\u8BC6\uFF1A\n" + filtered.map((r, i) => `[${i + 1}] ${r.content}`).join("\n");
3006
+ }
3007
+ } catch {
3008
+ }
3009
+ }
3010
+ const system = chatOpts.system ?? opts.system ?? "";
3011
+ const messages = [...chatOpts.messages ?? []];
3012
+ if (system) {
3013
+ const hasSystem = messages.some((m) => m.role === "system");
3014
+ if (!hasSystem) {
3015
+ messages.unshift({
3016
+ role: "system",
3017
+ content: knowledgeContext ? system + knowledgeContext : system
3018
+ });
3019
+ } else if (knowledgeContext) {
3020
+ const sysMsg = messages.find((m) => m.role === "system");
3021
+ if (sysMsg) sysMsg.content += knowledgeContext;
3022
+ }
3023
+ }
3024
+ if (prompt && !messages.some((m) => m.role === "user")) {
3025
+ messages.push({ role: "user", content: prompt });
3026
+ }
3027
+ const result = await generateText({
3028
+ model: opts.model,
3029
+ messages,
3030
+ tools: opts.tools,
3031
+ maxSteps: chatOpts.maxSteps ?? opts.maxSteps ?? 1,
3032
+ temperature: chatOpts.temperature ?? opts.temperature,
3033
+ maxTokens: chatOpts.maxTokens ?? opts.maxTokens,
3034
+ abortSignal: req.signal
3035
+ });
3036
+ return {
3037
+ text: result.text,
3038
+ toolCalls: result.toolCalls,
3039
+ toolResults: result.toolResults,
3040
+ steps: result.steps,
3041
+ finishReason: result.finishReason,
3042
+ usage: result.usage
3043
+ };
3044
+ },
3045
+ chatStreamResponse(streamOpts) {
3046
+ const encoder = new TextEncoder();
3047
+ const stream = new ReadableStream({
3048
+ async start(controller) {
3049
+ try {
3050
+ const result = await ctx.agent.chat(
3051
+ streamOpts.messages.filter((m) => m.role === "user").pop()?.content ?? "",
3052
+ {
3053
+ messages: streamOpts.messages,
3054
+ system: streamOpts.system
3055
+ }
3056
+ );
3057
+ controller.enqueue(encoder.encode(`data: ${JSON.stringify({ type: "text", content: result.text })}
3058
+
3059
+ `));
3060
+ controller.enqueue(encoder.encode("data: [DONE]\n\n"));
3061
+ controller.close();
3062
+ } catch (err) {
3063
+ controller.enqueue(encoder.encode(
3064
+ `data: ${JSON.stringify({ type: "error", error: String(err) })}
3065
+
3066
+ `
3067
+ ));
3068
+ controller.close();
3069
+ }
3070
+ }
3071
+ });
3072
+ return new Response(stream, {
3073
+ headers: {
3074
+ "Content-Type": "text/event-stream",
3075
+ "Cache-Control": "no-cache",
3076
+ Connection: "keep-alive"
3077
+ }
3078
+ });
3079
+ }
3080
+ };
3081
+ return next(req, ctx);
3082
+ };
3083
+ }
2797
3084
  export {
2798
3085
  DEFAULT_MAX_BODY,
2799
3086
  ErrorBoundary,
@@ -2802,6 +3089,9 @@ export {
2802
3089
  MIGRATIONS_TABLE,
2803
3090
  Router,
2804
3091
  ServerDataContext,
3092
+ agent,
3093
+ ai,
3094
+ auth,
2805
3095
  compress,
2806
3096
  cors,
2807
3097
  createHub,
@@ -2818,6 +3108,7 @@ export {
2818
3108
  reactRouter,
2819
3109
  redis,
2820
3110
  runWithTrace,
3111
+ sandbox,
2821
3112
  serve,
2822
3113
  serveStatic,
2823
3114
  tailwindDev,
@@ -0,0 +1,70 @@
1
+ import type { Middleware, User, Context } 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. `ctx` has `ctx.sql`, `ctx.redis`, etc. */
27
+ loadUser: (data: Record<string, unknown>, ctx: Context) => 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
+ /** Query parameter name (e.g. 'api_key'). */
41
+ query?: string;
42
+ /** Validate an API key → User or null. `ctx` has `ctx.sql`, `ctx.redis`, etc. */
43
+ validate: (key: string, ctx: Context) => Promise<User | null> | User | null;
44
+ };
45
+ }
46
+ /**
47
+ * Authentication middleware — injects `ctx.user`.
48
+ *
49
+ * Supports JWT (HS256), signed session cookies, and API keys.
50
+ * When no user is authenticated, requests still proceed —
51
+ * check `ctx.user` in your handlers to enforce auth.
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * // JWT
56
+ * app.use(auth({ jwt: { secret: 'my-secret' } }))
57
+ *
58
+ * // Session cookie
59
+ * app.use(auth({
60
+ * session: {
61
+ * secret: '...',
62
+ * loadUser: async (data, ctx) => ctx.sql`SELECT * FROM users WHERE id = ${data.userId}`,
63
+ * },
64
+ * }))
65
+ *
66
+ * // API key
67
+ * app.use(auth({ apiKey: { validate: async (key, ctx) => ctx.sql`...` } }))
68
+ * ```
69
+ */
70
+ 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.1",
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",