weifuwu 0.25.0 → 0.25.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -5,6 +5,6 @@ loadEnv()
5
5
  const port = Number(process.env.PORT) || 3000
6
6
  const srv = serve(app.handler(), { port, websocket: app.websocketHandler(), shutdown: false })
7
7
  process.on('SIGINT', () => {
8
- srv.stop()
8
+ srv.close()
9
9
  process.exit(0)
10
10
  })
@@ -5,9 +5,9 @@ import type { AIProvider } from '../ai/provider.ts';
5
5
  import type { RunParams, RunResult, KnowledgeDoc } from './types.ts';
6
6
  interface RunnerDeps {
7
7
  sql: SqlClient;
8
- agents: BoundTable<any>;
9
- runs: BoundTable<any>;
10
- knowledge: BoundTable<any>;
8
+ agents: BoundTable<Record<string, unknown>>;
9
+ runs: BoundTable<Record<string, unknown>>;
10
+ knowledge: BoundTable<Record<string, unknown>>;
11
11
  provider: AIProvider;
12
12
  modelName?: string;
13
13
  userTools?: Record<string, Tool>;
@@ -1,13 +1,22 @@
1
1
  import type { Middleware, Closeable } from './types.ts';
2
2
  import { Router } from './router.ts';
3
+ /** Tagged-template SQL function from the postgres.js library. */
4
+ type PgSql = (strings: TemplateStringsArray, ...values: unknown[]) => Promise<unknown[]>;
5
+ /** Schema table builder callback. */
6
+ type PgTable = (name: string, cols: Record<string, unknown>) => {
7
+ create(): Promise<void>;
8
+ createIndex(cols: string[], opts: {
9
+ unique: boolean;
10
+ }): Promise<void>;
11
+ };
3
12
  /** Options for {@link analytics}. */
4
13
  export interface AnalyticsOptions {
5
14
  /** Path prefixes to exclude from analytics (default: `['/__analytics', '/__wfw', '/static']`). */
6
15
  excluded?: string[];
7
16
  /** PostgreSQL client for persistent storage. Required for production use. */
8
17
  pg?: {
9
- sql: (strings: TemplateStringsArray, ...values: any[]) => Promise<any[]>;
10
- table: (name: string, cols: any) => any;
18
+ sql: PgSql;
19
+ table: PgTable;
11
20
  };
12
21
  }
13
22
  /** Analytics module returned by {@link analytics}. */
@@ -33,3 +42,4 @@ export interface AnalyticsModule extends Router, Closeable {
33
42
  * ```
34
43
  */
35
44
  export declare function analytics(options?: AnalyticsOptions): AnalyticsModule;
45
+ export {};
package/dist/compile.d.ts CHANGED
@@ -13,5 +13,3 @@ export declare function compile(path: string): Promise<any>;
13
13
  export declare let vendorHash: string;
14
14
  /** Build a single vendor bundle containing all needed vendor modules */
15
15
  export declare function compileVendorBundle(): Promise<string>;
16
- /** Clean up esbuild's internal worker pool. Call when you're done compiling. */
17
- export declare function closeCompile(): Promise<void>;
package/dist/csrf.d.ts CHANGED
@@ -8,6 +8,8 @@ export interface CsrfInjected {
8
8
  token: string;
9
9
  }
10
10
  /** Options for {@link csrf}. */
11
+ /** CSRF protection module — a {@link Middleware} that injects `ctx.csrf`. */
12
+ export type CsrfModule = Middleware<Context, Context & CsrfInjected>;
11
13
  export interface CsrfOptions {
12
14
  /** Cookie name for CSRF token (default: `'_csrf'`). */
13
15
  cookie?: string;
package/dist/flash.d.ts CHANGED
@@ -29,6 +29,8 @@ declare module './types.ts' {
29
29
  flash: FlashInjected;
30
30
  }
31
31
  }
32
+ /** Flash message module — a {@link Middleware} that injects `ctx.flash`. */
33
+ export type FlashModule = Middleware<Context, Context & FlashInjected>;
32
34
  /** Options for {@link flash}. */
33
35
  export interface FlashOptions {
34
36
  /**
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';
@@ -14,11 +15,11 @@ export type { CORSOptions } from './cors.ts';
14
15
  export { serveStatic } from './static.ts';
15
16
  export type { ServeStaticOptions } from './static.ts';
16
17
  export { validate } from './validate.ts';
17
- export type { ValidationSchemas } from './validate.ts';
18
+ export type { ValidationSchemas, ValidateModule } from './validate.ts';
18
19
  export { getCookies, setCookie, deleteCookie } from './cookie.ts';
19
20
  export type { CookieOptions } from './cookie.ts';
20
21
  export { upload } from './upload.ts';
21
- export type { UploadOptions, UploadedFile } from './upload.ts';
22
+ export type { UploadOptions, UploadedFile, UploadModule } from './upload.ts';
22
23
  export { rateLimit } from './rate-limit.ts';
23
24
  export type { RateLimitOptions } from './rate-limit.ts';
24
25
  export { compress } from './compress.ts';
@@ -26,7 +27,7 @@ export type { CompressOptions } from './compress.ts';
26
27
  export { helmet } from './helmet.ts';
27
28
  export type { HelmetOptions } from './helmet.ts';
28
29
  export { requestId } from './request-id.ts';
29
- export type { RequestIdOptions } from './request-id.ts';
30
+ export type { RequestIdOptions, RequestIdModule } from './request-id.ts';
30
31
  export { createSSEStream, formatSSE, formatSSEData } from './sse.ts';
31
32
  export type { SSEEvent } from './sse.ts';
32
33
  export { testApp, TestApp, TestRequest, createTestDb, withTestDb } from './test-utils.ts';
@@ -69,13 +70,13 @@ export type { ThemeOptions, ThemeInjected } from './theme.ts';
69
70
  export { i18n } from './i18n.ts';
70
71
  export type { I18nOptions, I18nInjected } from './i18n.ts';
71
72
  export { flash } from './flash.ts';
72
- export type { FlashOptions, FlashInjected } from './flash.ts';
73
+ export type { FlashOptions, FlashInjected, FlashModule } from './flash.ts';
73
74
  export { seo, seoMiddleware, seoTags } from './seo.ts';
74
75
  export type { SeoOptions, RobotsRule, SitemapUrl, SitemapConfig, SeoHeadersConfig, SeoTagsConfig, } from './seo.ts';
75
76
  export { mailer } from './mailer.ts';
76
77
  export type { MailerOptions, MailOptions, Mailer } from './mailer.ts';
77
78
  export { csrf } from './csrf.ts';
78
- export type { CsrfOptions, CsrfInjected } from './csrf.ts';
79
+ export type { CsrfOptions, CsrfInjected, CsrfModule } from './csrf.ts';
79
80
  export { logdb } from './logdb/index.ts';
80
81
  export type { LogdbOptions, LogdbModule, LogEntry, LogEntryInput } from './logdb/types.ts';
81
82
  export { iii, createWorker, registerWorker } from './iii/index.ts';
package/dist/index.js CHANGED
@@ -4,6 +4,16 @@ var __export = (target, all) => {
4
4
  __defProp(target, name, { get: all[name], enumerable: true });
5
5
  };
6
6
 
7
+ // types.ts
8
+ var HttpError = class extends Error {
9
+ status;
10
+ constructor(message, status) {
11
+ super(message);
12
+ this.name = "HttpError";
13
+ this.status = status;
14
+ }
15
+ };
16
+
7
17
  // trace.ts
8
18
  import crypto2 from "node:crypto";
9
19
  import { AsyncLocalStorage } from "node:async_hooks";
@@ -110,14 +120,6 @@ function env() {
110
120
 
111
121
  // serve.ts
112
122
  import http from "node:http";
113
- var HttpError = class extends Error {
114
- status;
115
- constructor(message, status) {
116
- super(message);
117
- this.status = status;
118
- this.name = "HttpError";
119
- }
120
- };
121
123
  var DEFAULT_MAX_BODY = 10 * 1024 * 1024;
122
124
  async function readBody(req, maxSize) {
123
125
  const limit = maxSize ?? DEFAULT_MAX_BODY;
@@ -254,6 +256,7 @@ function serve(handler, options) {
254
256
  resolveReady();
255
257
  return {
256
258
  stop: () => Promise.resolve(),
259
+ close: () => Promise.resolve(),
257
260
  ready,
258
261
  get port() {
259
262
  return 0;
@@ -287,30 +290,29 @@ function serve(handler, options) {
287
290
  const displayHost = _cachedHostname === "0.0.0.0" ? "localhost" : _cachedHostname || "localhost";
288
291
  console.log(`weifuwu listening on http://${displayHost}:${_cachedPort}`);
289
292
  });
290
- return {
291
- stop: (timeoutMs = 1e4) => {
292
- if (shutdownHandler) {
293
- process.off("SIGTERM", shutdownHandler);
294
- process.off("SIGINT", shutdownHandler);
295
- shutdownHandler = null;
296
- }
297
- return new Promise((resolve16) => {
298
- if (!server.listening) {
299
- resolve16();
300
- return;
301
- }
302
- server.close();
303
- server.closeIdleConnections();
304
- const timer = setTimeout(() => {
305
- server.closeAllConnections();
306
- resolve16();
307
- }, timeoutMs);
308
- server.on("close", () => {
309
- clearTimeout(timer);
310
- resolve16();
311
- });
293
+ async function stop(timeoutMs = 1e4) {
294
+ if (shutdownHandler) {
295
+ process.off("SIGTERM", shutdownHandler);
296
+ process.off("SIGINT", shutdownHandler);
297
+ shutdownHandler = null;
298
+ }
299
+ if (!server.listening) return;
300
+ server.close();
301
+ server.closeIdleConnections();
302
+ return new Promise((resolve16) => {
303
+ const timer = setTimeout(() => {
304
+ server.closeAllConnections();
305
+ resolve16();
306
+ }, timeoutMs);
307
+ server.on("close", () => {
308
+ clearTimeout(timer);
309
+ resolve16();
312
310
  });
313
- },
311
+ });
312
+ }
313
+ return {
314
+ close: stop,
315
+ stop,
314
316
  ready,
315
317
  get port() {
316
318
  if (!server.listening) return 0;
@@ -517,7 +519,7 @@ var Router = class _Router {
517
519
  * ```
518
520
  */
519
521
  _checkMiddlewareMeta(mw, location) {
520
- const meta = mw.__meta ?? mw.middleware?.().__meta;
522
+ const meta = mw.__meta ?? (typeof mw === "object" && mw && "middleware" in mw ? mw.middleware().__meta : void 0);
521
523
  if (!meta) return;
522
524
  for (const dep of meta.depends) {
523
525
  if (!this._ctxFields.has(dep)) {
@@ -1271,7 +1273,7 @@ function parseBody(text2, ct) {
1271
1273
  return text2;
1272
1274
  }
1273
1275
  function validate(schemas) {
1274
- return async (req, ctx, next) => {
1276
+ const mw = async (req, ctx, next) => {
1275
1277
  const parsed = {};
1276
1278
  const issues = [];
1277
1279
  if (schemas?.params) {
@@ -1358,6 +1360,8 @@ function validate(schemas) {
1358
1360
  ctx.parsed = { ...ctx.parsed, ...parsed };
1359
1361
  return next(req, ctx);
1360
1362
  };
1363
+ mw.__meta = { injects: ["parsed"], depends: [] };
1364
+ return mw;
1361
1365
  }
1362
1366
 
1363
1367
  // cookie.ts
@@ -1461,7 +1465,7 @@ function detectMimeFromExtension(filename) {
1461
1465
  }
1462
1466
  function upload(options) {
1463
1467
  const saveDir = options?.dir;
1464
- return async (req, ctx, next) => {
1468
+ const mw = async (req, ctx, next) => {
1465
1469
  const ct = req.headers.get("content-type") ?? "";
1466
1470
  if (!ct.includes("multipart/form-data")) return next(req, ctx);
1467
1471
  try {
@@ -1517,6 +1521,8 @@ function upload(options) {
1517
1521
  ctx.parsed = { ...ctx.parsed, files, fields };
1518
1522
  return next(req, ctx);
1519
1523
  };
1524
+ mw.__meta = { injects: ["parsed"], depends: [] };
1525
+ return mw;
1520
1526
  }
1521
1527
 
1522
1528
  // rate-limit.ts
@@ -1601,7 +1607,7 @@ function rateLimit(options) {
1601
1607
  return addRateLimitHeaders(res, max, remaining, reset);
1602
1608
  };
1603
1609
  mw.__meta = { injects: [], depends: [] };
1604
- mw.close = () => {
1610
+ mw.close = async () => {
1605
1611
  if (interval) clearInterval(interval);
1606
1612
  hits.clear();
1607
1613
  };
@@ -1728,7 +1734,7 @@ import crypto3 from "node:crypto";
1728
1734
  function requestId(options) {
1729
1735
  const header = options?.header ?? "X-Request-ID";
1730
1736
  const gen = options?.generator ?? (() => crypto3.randomUUID());
1731
- return async (req, ctx, next) => {
1737
+ const mw = async (req, ctx, next) => {
1732
1738
  const existing = req.headers.get(header);
1733
1739
  const id2 = existing ?? gen();
1734
1740
  ctx.requestId = id2;
@@ -1738,6 +1744,8 @@ function requestId(options) {
1738
1744
  h.set(header, id2);
1739
1745
  return new Response(res.body, { status: res.status, statusText: res.statusText, headers: h });
1740
1746
  };
1747
+ mw.__meta = { injects: ["requestId"], depends: [] };
1748
+ return mw;
1741
1749
  }
1742
1750
 
1743
1751
  // sse.ts
@@ -1763,7 +1771,7 @@ function createSSEStream(iterable, opts) {
1763
1771
  controller.enqueue(encoder.encode(text2));
1764
1772
  }
1765
1773
  } catch (e) {
1766
- if (e.name !== "AbortError") {
1774
+ if (e instanceof Error && e.name !== "AbortError") {
1767
1775
  controller.enqueue(encoder.encode(formatSSE("error", { error: e.message })));
1768
1776
  }
1769
1777
  } finally {
@@ -1833,6 +1841,7 @@ var TestRequest = class {
1833
1841
  }
1834
1842
  /** Shortcut: set ctx.user */
1835
1843
  withUser(user2) {
1844
+ ;
1836
1845
  this.ctxMixin.user = user2;
1837
1846
  return this;
1838
1847
  }
@@ -1999,7 +2008,7 @@ var TestApp = class {
1999
2008
  }
2000
2009
  this.wsConnections = [];
2001
2010
  if (this.wsServer) {
2002
- this.wsServer.stop();
2011
+ this.wsServer.close();
2003
2012
  this.wsServer = null;
2004
2013
  }
2005
2014
  }
@@ -2413,7 +2422,16 @@ async function getStreamObject() {
2413
2422
  async function aiStream(handler, provider) {
2414
2423
  const r = new Router();
2415
2424
  r.post("/", async (req, ctx) => {
2416
- const options = await handler(req, ctx);
2425
+ let options;
2426
+ try {
2427
+ options = await handler(req, ctx);
2428
+ } catch (err) {
2429
+ const message = err instanceof Error ? err.message : String(err);
2430
+ return new Response(JSON.stringify({ error: message }), {
2431
+ status: 500,
2432
+ headers: { "Content-Type": "application/json" }
2433
+ });
2434
+ }
2417
2435
  if (provider && !options.model) {
2418
2436
  options.model = provider.model();
2419
2437
  }
@@ -3819,7 +3837,8 @@ function registerOAuthLoginRoutes(router, deps, providers) {
3819
3837
  if (email) {
3820
3838
  const existingUser = await deps.findUserByEmail(email);
3821
3839
  if (existingUser) {
3822
- await linkProvider(existingUser.id, provider, providerId, email, name, avatarUrl);
3840
+ const uid = existingUser.id;
3841
+ await linkProvider(uid, provider, providerId, email, name, avatarUrl);
3823
3842
  return existingUser;
3824
3843
  }
3825
3844
  }
@@ -3827,7 +3846,8 @@ function registerOAuthLoginRoutes(router, deps, providers) {
3827
3846
  email || `${provider}_${providerId}@oauth.local`,
3828
3847
  name || provider
3829
3848
  );
3830
- await linkProvider(newUser.id, provider, providerId, email, name, avatarUrl);
3849
+ const userId2 = newUser.id;
3850
+ await linkProvider(userId2, provider, providerId, email, name, avatarUrl);
3831
3851
  return newUser;
3832
3852
  }
3833
3853
  function getProviderMeta(providerName) {
@@ -3857,8 +3877,9 @@ function registerOAuthLoginRoutes(router, deps, providers) {
3857
3877
  const state = crypto5.randomUUID();
3858
3878
  const redirectUri = new URL(req.url);
3859
3879
  redirectUri.pathname = redirectUri.pathname.replace(/\/[^/]+$/, "/") + providerName + "/callback";
3860
- if (ctx.session) {
3861
- ctx.session.oauthState = { state, provider: providerName };
3880
+ const sess = ctx.session;
3881
+ if (sess) {
3882
+ sess.oauthState = { state, provider: providerName };
3862
3883
  }
3863
3884
  const scope = config.scope ?? meta.scope;
3864
3885
  const params = new URLSearchParams({
@@ -3885,11 +3906,12 @@ function registerOAuthLoginRoutes(router, deps, providers) {
3885
3906
  if (!code || !state) {
3886
3907
  return Response.json({ error: "Missing code or state parameter" }, { status: 400 });
3887
3908
  }
3888
- const savedState = ctx.session?.oauthState;
3909
+ const sess = ctx.session;
3910
+ const savedState = sess?.oauthState;
3889
3911
  if (!savedState || savedState.state !== state || savedState.provider !== providerName) {
3890
3912
  return Response.json({ error: "Invalid state \u2014 possible CSRF attack" }, { status: 403 });
3891
3913
  }
3892
- if (ctx.session) delete ctx.session.oauthState;
3914
+ if (sess) delete sess.oauthState;
3893
3915
  const redirectUri = url.origin + url.pathname.replace(/\/callback$/, "");
3894
3916
  let tokenRes;
3895
3917
  try {
@@ -3947,9 +3969,10 @@ function registerOAuthLoginRoutes(router, deps, providers) {
3947
3969
  return Response.json({ error: "Failed to create/link user" }, { status: 500 });
3948
3970
  }
3949
3971
  const token = signToken(user2);
3950
- if (ctx.session) {
3951
- ctx.session.userId = user2.id;
3952
- ctx.session.role = user2.role;
3972
+ const sess2 = ctx.session;
3973
+ if (sess2) {
3974
+ sess2.userId = user2.id;
3975
+ sess2.role = user2.role;
3953
3976
  }
3954
3977
  const accept = req.headers.get("accept") ?? "";
3955
3978
  if (accept.includes("application/json")) {
@@ -4147,9 +4170,7 @@ function user(options) {
4147
4170
  const { email, password, name } = RegisterSchema.parse(data);
4148
4171
  const existing = await findByEmail(email);
4149
4172
  if (existing) {
4150
- const err = new Error("Email already registered");
4151
- err.status = 409;
4152
- throw err;
4173
+ throw new HttpError("Email already registered", 409);
4153
4174
  }
4154
4175
  const hashed = hashPassword(password);
4155
4176
  const row = await _users.insert({ email, password: hashed, name });
@@ -4162,14 +4183,10 @@ function user(options) {
4162
4183
  const { data: rows } = await _users.readMany({ email });
4163
4184
  const row = rows[0];
4164
4185
  if (!row) {
4165
- const err = new Error("Invalid email or password");
4166
- err.status = 401;
4167
- throw err;
4186
+ throw new HttpError("Invalid email or password", 401);
4168
4187
  }
4169
4188
  if (!verifyPassword(password, row.password)) {
4170
- const err = new Error("Invalid email or password");
4171
- err.status = 401;
4172
- throw err;
4189
+ throw new HttpError("Invalid email or password", 401);
4173
4190
  }
4174
4191
  const userData = row;
4175
4192
  const token = signToken(userData);
@@ -4359,7 +4376,7 @@ function user(options) {
4359
4376
  return null;
4360
4377
  }
4361
4378
  function middleware() {
4362
- return async (req, ctx, next) => {
4379
+ const mw = async (req, ctx, next) => {
4363
4380
  const userData = await resolveUser(req, ctx);
4364
4381
  if (userData) {
4365
4382
  ctx.user = userData;
@@ -4370,9 +4387,11 @@ function user(options) {
4370
4387
  headers: headerName.toLowerCase() === "authorization" ? { "WWW-Authenticate": "Bearer" } : void 0
4371
4388
  });
4372
4389
  };
4390
+ mw.__meta = { injects: ["user"], depends: [] };
4391
+ return mw;
4373
4392
  }
4374
4393
  function middlewareOptional(_opts) {
4375
- return async (req, ctx, next) => {
4394
+ const mw = async (req, ctx, next) => {
4376
4395
  const userData = await resolveUser(req, ctx);
4377
4396
  if (userData) {
4378
4397
  ;
@@ -4380,6 +4399,8 @@ function user(options) {
4380
4399
  }
4381
4400
  return next(req, ctx);
4382
4401
  };
4402
+ mw.__meta = { injects: ["user"], depends: [] };
4403
+ return mw;
4383
4404
  }
4384
4405
  async function parseBody2(req) {
4385
4406
  const ct = req.headers.get("content-type") || "";
@@ -4404,8 +4425,9 @@ function user(options) {
4404
4425
  if (err instanceof z2.ZodError) {
4405
4426
  return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
4406
4427
  }
4407
- const status = err.status ?? 500;
4408
- return Response.json({ error: err.message }, { status });
4428
+ const status = err instanceof HttpError ? err.status : 500;
4429
+ const message = err instanceof Error ? err.message : String(err);
4430
+ return Response.json({ error: message }, { status });
4409
4431
  }
4410
4432
  });
4411
4433
  r.post("/login", async (req, ctx) => {
@@ -4426,8 +4448,9 @@ function user(options) {
4426
4448
  if (err instanceof z2.ZodError) {
4427
4449
  return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
4428
4450
  }
4429
- const status = err.status ?? 500;
4430
- return Response.json({ error: err.message }, { status });
4451
+ const status = err instanceof HttpError ? err.status : 500;
4452
+ const message = err instanceof Error ? err.message : String(err);
4453
+ return Response.json({ error: message }, { status });
4431
4454
  }
4432
4455
  });
4433
4456
  }
@@ -4446,7 +4469,8 @@ function user(options) {
4446
4469
  if (err instanceof z2.ZodError) {
4447
4470
  return Response.json({ error: "Validation failed", issues: err.issues }, { status: 400 });
4448
4471
  }
4449
- return Response.json({ error: err.message }, { status: 500 });
4472
+ const message = err instanceof Error ? err.message : String(err);
4473
+ return Response.json({ error: message }, { status: 500 });
4450
4474
  }
4451
4475
  });
4452
4476
  r.delete("/api-keys/:id", middleware(), async (req, ctx) => {
@@ -4522,7 +4546,7 @@ function user(options) {
4522
4546
  return mod;
4523
4547
  }
4524
4548
 
4525
- // redis/index.ts
4549
+ // redis/client.ts
4526
4550
  import { Redis as IORedis } from "ioredis";
4527
4551
  function redis(opts) {
4528
4552
  const options = typeof opts === "string" ? { url: opts } : opts ?? {};
@@ -4718,15 +4742,12 @@ function createMemoryQueue(opts) {
4718
4742
  running = true;
4719
4743
  poll();
4720
4744
  };
4721
- mw.stop = function stop2() {
4745
+ mw.close = async function close() {
4722
4746
  running = false;
4723
4747
  if (pollTimer) {
4724
4748
  clearTimeout(pollTimer);
4725
4749
  pollTimer = null;
4726
4750
  }
4727
- };
4728
- mw.close = async function close() {
4729
- mw.stop();
4730
4751
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
4731
4752
  };
4732
4753
  mw.jobs = async function(limit) {
@@ -4891,15 +4912,12 @@ function createPgQueue(opts) {
4891
4912
  running = true;
4892
4913
  poll();
4893
4914
  };
4894
- mw.stop = function stop2() {
4915
+ mw.close = async function close() {
4895
4916
  running = false;
4896
4917
  if (pollTimer) {
4897
4918
  clearTimeout(pollTimer);
4898
4919
  pollTimer = null;
4899
4920
  }
4900
- };
4901
- mw.close = async function close() {
4902
- mw.stop();
4903
4921
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
4904
4922
  };
4905
4923
  mw.jobs = async function jobs(limit) {
@@ -5058,16 +5076,13 @@ function createRedisQueue(opts) {
5058
5076
  running = true;
5059
5077
  poll();
5060
5078
  };
5061
- mw.stop = function stop2() {
5079
+ mw.close = async function close() {
5062
5080
  running = false;
5063
5081
  epoch++;
5064
5082
  if (pollTimer) {
5065
5083
  clearTimeout(pollTimer);
5066
5084
  pollTimer = null;
5067
5085
  }
5068
- };
5069
- mw.close = async function close() {
5070
- mw.stop();
5071
5086
  while (inflight > 0) await new Promise((r) => setTimeout(r, 50));
5072
5087
  redis2.disconnect();
5073
5088
  };
@@ -6065,7 +6080,7 @@ function tenant(options) {
6065
6080
  );
6066
6081
  }
6067
6082
  function middleware() {
6068
- return async (req, ctx, next) => {
6083
+ const mw = async (req, ctx, next) => {
6069
6084
  const user2 = ctx.user;
6070
6085
  if (!user2) {
6071
6086
  return new Response("Unauthorized", { status: 401 });
@@ -6101,6 +6116,8 @@ function tenant(options) {
6101
6116
  ctx.tenant = { id: member.id, name: member.name, role: member.role };
6102
6117
  return next(req, ctx);
6103
6118
  };
6119
+ mw.__meta = { injects: ["tenant"], depends: ["user"] };
6120
+ return mw;
6104
6121
  }
6105
6122
  const r = buildRouter(sql2, usersTable);
6106
6123
  const mod = r;
@@ -6179,17 +6196,13 @@ function buildRouter2(deps) {
6179
6196
  if (!body.input && !body.messages) {
6180
6197
  return Response.json({ error: "input or messages is required" }, { status: 400 });
6181
6198
  }
6182
- try {
6183
- const result = await runner.run(id2, body);
6184
- if ("stream" in result) {
6185
- return new Response(result.stream, {
6186
- headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }
6187
- });
6188
- }
6189
- return Response.json(result);
6190
- } catch (err) {
6191
- return Response.json({ error: err.message }, { status: 500 });
6199
+ const result = await runner.run(id2, body);
6200
+ if ("stream" in result) {
6201
+ return new Response(result.stream, {
6202
+ headers: { "Content-Type": "text/event-stream", "Cache-Control": "no-cache" }
6203
+ });
6192
6204
  }
6205
+ return Response.json(result);
6193
6206
  });
6194
6207
  r.get("/agents/:id/runs", async (_req, ctx) => {
6195
6208
  const agentId = parseInt(ctx.params.id, 10);
@@ -6220,13 +6233,26 @@ function buildRouter2(deps) {
6220
6233
  { orderBy: { created_at: "desc" } }
6221
6234
  );
6222
6235
  const total = rows.length;
6223
- const success = rows.filter((r2) => r2.status === "success" || r2.status === "stream").length;
6236
+ const success = rows.filter(
6237
+ (r2) => r2.status === "success" || r2.status === "stream"
6238
+ ).length;
6224
6239
  const error = rows.filter((r2) => r2.status === "error").length;
6225
- const totalTokensIn = rows.reduce((sum, r2) => sum + (r2.tokens_in || 0), 0);
6226
- const totalTokensOut = rows.reduce((sum, r2) => sum + (r2.tokens_out || 0), 0);
6227
- const totalElapsed = rows.reduce((sum, r2) => sum + (r2.elapsed_ms || 0), 0);
6240
+ const totalTokensIn = rows.reduce(
6241
+ (sum, r2) => sum + (r2.tokens_in || 0),
6242
+ 0
6243
+ );
6244
+ const totalTokensOut = rows.reduce(
6245
+ (sum, r2) => sum + (r2.tokens_out || 0),
6246
+ 0
6247
+ );
6248
+ const totalElapsed = rows.reduce(
6249
+ (sum, r2) => sum + (r2.elapsed_ms || 0),
6250
+ 0
6251
+ );
6228
6252
  const avgElapsed = total > 0 ? Math.round(totalElapsed / total) : 0;
6229
- const sorted = [...rows].sort((a, b) => (a.elapsed_ms || 0) - (b.elapsed_ms || 0));
6253
+ const sorted = [...rows].sort(
6254
+ (a, b) => (a.elapsed_ms || 0) - (b.elapsed_ms || 0)
6255
+ );
6230
6256
  const p95Idx = Math.ceil(sorted.length * 0.95) - 1;
6231
6257
  const p95Elapsed = sorted.length > 0 ? sorted[p95Idx]?.elapsed_ms || 0 : 0;
6232
6258
  return Response.json({
@@ -6252,7 +6278,10 @@ function buildRouter2(deps) {
6252
6278
  const doc = await runner.addKnowledge(agentId, body.title || "", body.content);
6253
6279
  return Response.json(doc, { status: 201 });
6254
6280
  } catch (err) {
6255
- return Response.json({ error: err.message }, { status: 500 });
6281
+ return Response.json(
6282
+ { error: err instanceof Error ? err.message : String(err) },
6283
+ { status: 500 }
6284
+ );
6256
6285
  }
6257
6286
  });
6258
6287
  r.get("/agents/:id/knowledge", async (_req, ctx) => {
@@ -6304,7 +6333,12 @@ async function searchKnowledge(sql2, provider, agentId, query, limit = 5) {
6304
6333
  `SELECT id, title, content, metadata, embedding <=> $1::vector AS _score FROM "_knowledge_documents" WHERE agent_id = $2 ORDER BY embedding <=> $1::vector LIMIT $3`,
6305
6334
  [vec, agentId, limit]
6306
6335
  );
6307
- return docs.map((d) => ({ id: d.id, title: d.title, content: d.content, score: d._score }));
6336
+ return docs.map((d) => ({
6337
+ id: d.id,
6338
+ title: d.title,
6339
+ content: d.content,
6340
+ score: d._score
6341
+ }));
6308
6342
  }
6309
6343
  async function loadAgent(agents, agentId) {
6310
6344
  const row = await agents.read(agentId);
@@ -7349,7 +7383,7 @@ async function deploy(config) {
7349
7383
  await stopProcess({ child: app.process, port: app.currentPort });
7350
7384
  }
7351
7385
  }
7352
- httpServer?.stop();
7386
+ httpServer?.close();
7353
7387
  },
7354
7388
  ready: httpServer.ready,
7355
7389
  url: `http://localhost:${config.port}/`,
@@ -7734,11 +7768,12 @@ function buildHeadPayload(opts) {
7734
7768
  flash: ctx.flash,
7735
7769
  loaderData
7736
7770
  };
7737
- if (ctx.user && typeof ctx.user === "object") {
7771
+ const rawUser = ctx.user;
7772
+ if (rawUser && typeof rawUser === "object") {
7738
7773
  const safeUser = {};
7739
7774
  for (const k of ["id", "name", "email", "role", "avatar"]) {
7740
- if (k in ctx.user) {
7741
- safeUser[k] = ctx.user[k];
7775
+ if (k in rawUser) {
7776
+ safeUser[k] = rawUser[k];
7742
7777
  }
7743
7778
  }
7744
7779
  ctxData.user = safeUser;
@@ -8049,7 +8084,6 @@ function moduleServer(opts) {
8049
8084
  _setImportRoots(roots);
8050
8085
  const router = new Router();
8051
8086
  router.get("/__wfw/m/*", (async (req, ctx) => {
8052
- const reqUrl = new URL(req.url);
8053
8087
  const filePath = (ctx.params["*"] || "").split("?")[0];
8054
8088
  const ext = filePath.split(".").pop();
8055
8089
  if (ext !== "tsx" && ext !== "ts") {
@@ -8619,25 +8653,22 @@ async function getSession(sql2, id2) {
8619
8653
  }
8620
8654
  async function listSessions(sql2, userId2) {
8621
8655
  const opts = { orderBy: { updated_at: "desc" } };
8622
- if (userId2 !== void 0) {
8623
- const { data: rows2 } = await sessions.readMany(
8624
- sql2,
8625
- { user_id: userId2, active: true },
8626
- opts
8627
- );
8628
- return rows2;
8629
- }
8630
- const { data: rows } = await sessions.readMany(sql2, { active: true }, opts);
8656
+ const filter = userId2 !== void 0 ? { user_id: userId2, active: true } : { active: true };
8657
+ const { data: rows } = await sessions.readMany(sql2, filter, opts);
8631
8658
  return rows;
8632
8659
  }
8633
8660
  async function deleteSession(sql2, id2) {
8634
8661
  await sessions.update(sql2, id2, { active: false, updated_at: sql`NOW()` });
8635
8662
  }
8636
8663
  async function getHistory(sql2, sessionId, limit = 50) {
8637
- const { data: rows } = await messages.readMany(sql2, { session_id: sessionId }, {
8638
- orderBy: { created_at: "asc" },
8639
- limit
8640
- });
8664
+ const { data: rows } = await messages.readMany(
8665
+ sql2,
8666
+ { session_id: sessionId },
8667
+ {
8668
+ orderBy: { created_at: "asc" },
8669
+ limit
8670
+ }
8671
+ );
8641
8672
  return rows;
8642
8673
  }
8643
8674
  async function addTextMessage(sql2, sessionId, role, content, tokensIn = 0, tokensOut = 0) {
@@ -8838,7 +8869,7 @@ function createBashTool(ctx) {
8838
8869
  // opencode/tools/read.ts
8839
8870
  import { tool as tool4 } from "ai";
8840
8871
  import { z as z6 } from "zod";
8841
- import { readFileSync as readFileSync7 } from "node:fs";
8872
+ import { readFileSync as readFileSync6 } from "node:fs";
8842
8873
  import { resolve as resolve9 } from "node:path";
8843
8874
  function createReadTool(ctx) {
8844
8875
  return tool4({
@@ -8853,7 +8884,7 @@ function createReadTool(ctx) {
8853
8884
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
8854
8885
  return { error: "Path not allowed", content: null, totalLines: 0 };
8855
8886
  }
8856
- const content = readFileSync7(resolved, "utf-8");
8887
+ const content = readFileSync6(resolved, "utf-8");
8857
8888
  const lines = content.split("\n");
8858
8889
  const totalLines = lines.length;
8859
8890
  if (offset !== void 0) {
@@ -8904,7 +8935,7 @@ function createWriteTool(ctx) {
8904
8935
  // opencode/tools/edit.ts
8905
8936
  import { tool as tool6 } from "ai";
8906
8937
  import { z as z8 } from "zod";
8907
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync3 } from "node:fs";
8938
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync3 } from "node:fs";
8908
8939
  import { resolve as resolve11 } from "node:path";
8909
8940
  function createEditTool(ctx) {
8910
8941
  return tool6({
@@ -8920,7 +8951,7 @@ function createEditTool(ctx) {
8920
8951
  if (!isPathAllowed(resolved, ctx.workspace, ctx.permissions.permissions)) {
8921
8952
  return { error: "Path not allowed" };
8922
8953
  }
8923
- const content = readFileSync8(resolved, "utf-8");
8954
+ const content = readFileSync7(resolved, "utf-8");
8924
8955
  if (replaceAll) {
8925
8956
  if (!content.includes(oldString)) {
8926
8957
  return { error: "oldString not found in file", replaced: 0 };
@@ -9250,17 +9281,17 @@ async function buildRouter4(deps) {
9250
9281
  FROM "_opencode_messages"
9251
9282
  WHERE session_id = ${sessionId}
9252
9283
  `;
9253
- const stats = rows[0] || {
9284
+ const raw = rows[0] || {
9254
9285
  message_count: 0,
9255
9286
  total_tokens_in: 0,
9256
9287
  total_tokens_out: 0
9257
9288
  };
9258
9289
  return Response.json({
9259
9290
  session_id: sessionId,
9260
- message_count: stats.message_count,
9261
- tokens_in: stats.total_tokens_in,
9262
- tokens_out: stats.total_tokens_out,
9263
- tokens_total: stats.total_tokens_in + stats.total_tokens_out
9291
+ message_count: raw.message_count,
9292
+ tokens_in: raw.total_tokens_in,
9293
+ tokens_out: raw.total_tokens_out,
9294
+ tokens_total: Number(raw.total_tokens_in) + Number(raw.total_tokens_out)
9264
9295
  });
9265
9296
  });
9266
9297
  try {
@@ -9316,8 +9347,13 @@ function createWSHandler2(deps) {
9316
9347
  client.mountPath
9317
9348
  );
9318
9349
  ws.send(JSON.stringify({ type: "session_created", session: session2 }));
9319
- } catch (e) {
9320
- ws.send(JSON.stringify({ type: "error", error: e.message }));
9350
+ } catch (err) {
9351
+ ws.send(
9352
+ JSON.stringify({
9353
+ type: "error",
9354
+ error: err instanceof Error ? err.message : String(err)
9355
+ })
9356
+ );
9321
9357
  }
9322
9358
  break;
9323
9359
  }
@@ -9371,9 +9407,9 @@ function createWSHandler2(deps) {
9371
9407
  break;
9372
9408
  }
9373
9409
  }
9374
- } catch (e) {
9375
- if (e.name !== "AbortError") {
9376
- ws.send(JSON.stringify({ type: "error", error: e.message }));
9410
+ } catch (err) {
9411
+ if (err instanceof Error && err.name !== "AbortError") {
9412
+ ws.send(JSON.stringify({ type: "error", error: err.message }));
9377
9413
  }
9378
9414
  }
9379
9415
  break;
@@ -9856,6 +9892,7 @@ function theme(options) {
9856
9892
  };
9857
9893
  return next(req, ctx);
9858
9894
  };
9895
+ mw.__meta = { injects: ["theme"], depends: [] };
9859
9896
  class ThemeRouter extends Router {
9860
9897
  middleware() {
9861
9898
  return mw;
@@ -9957,6 +9994,7 @@ function i18n(options) {
9957
9994
  };
9958
9995
  return next(req, ctx);
9959
9996
  };
9997
+ mw.__meta = { injects: ["i18n"], depends: [] };
9960
9998
  class I18nRouter extends Router {
9961
9999
  middleware() {
9962
10000
  return mw;
@@ -10001,7 +10039,7 @@ function makeSetFlash(name, location) {
10001
10039
  }
10002
10040
  function flash(options) {
10003
10041
  const name = options?.name ?? "flash";
10004
- return async (req, ctx, next) => {
10042
+ const mw = async (req, ctx, next) => {
10005
10043
  const raw = getCookies(req)[name] ?? null;
10006
10044
  const referer = req.headers.get("referer") || "/";
10007
10045
  let value = void 0;
@@ -10024,6 +10062,8 @@ function flash(options) {
10024
10062
  }
10025
10063
  return res;
10026
10064
  };
10065
+ mw.__meta = { injects: ["flash"], depends: [] };
10066
+ return mw;
10027
10067
  }
10028
10068
 
10029
10069
  // seo.ts
@@ -10206,7 +10246,7 @@ function csrf(options) {
10206
10246
  const headerName = options?.header ?? "x-csrf-token";
10207
10247
  const bodyKey = options?.key ?? "_csrf";
10208
10248
  const excluded = new Set(options?.excludeMethods ?? ["GET", "HEAD", "OPTIONS"]);
10209
- return async (req, ctx, next) => {
10249
+ const mw = async (req, ctx, next) => {
10210
10250
  const method = req.method.toUpperCase();
10211
10251
  if (excluded.has(method)) {
10212
10252
  let token = getCookies(req)[cookieName];
@@ -10243,6 +10283,8 @@ function csrf(options) {
10243
10283
  }
10244
10284
  return next(req, ctx);
10245
10285
  };
10286
+ mw.__meta = { injects: ["csrf"], depends: [] };
10287
+ return mw;
10246
10288
  }
10247
10289
 
10248
10290
  // logdb/rest.ts
@@ -11880,6 +11922,7 @@ function s3(options) {
11880
11922
  mw.url = url;
11881
11923
  mw.list = list;
11882
11924
  mw.client = client;
11925
+ mw.__meta = { injects: ["s3"], depends: [] };
11883
11926
  return mw;
11884
11927
  }
11885
11928
 
@@ -12142,6 +12185,7 @@ function permissions(options) {
12142
12185
  mw.requireRole = requireRole;
12143
12186
  mw.requirePermission = requirePermission;
12144
12187
  mw.migrate = migrate;
12188
+ mw.__meta = { injects: ["permissions"], depends: ["user"] };
12145
12189
  return mw;
12146
12190
  }
12147
12191
 
@@ -12615,6 +12659,7 @@ function notifier(opts) {
12615
12659
  }
12616
12660
  export {
12617
12661
  DEFAULT_MAX_BODY,
12662
+ HttpError,
12618
12663
  MIGRATIONS_TABLE,
12619
12664
  MemoryCache,
12620
12665
  MemoryStore,
@@ -1,6 +1,5 @@
1
1
  import { Router } from './router.ts';
2
2
  export declare function clearModuleCache(filePath?: string): void;
3
- export declare function _setImportRoots(roots: string[]): void;
4
3
  export declare function transformModule(absPath: string, root: string, mountPath?: string): Promise<{
5
4
  url: string;
6
5
  code: string;
@@ -1,8 +1,9 @@
1
1
  import { Router } from '../router.ts';
2
2
  import type { LanguageModel } from 'ai';
3
+ import type { SqlClient } from '../vendor.ts';
3
4
  import type { SkillDef, SkillRegistry, OpencodePermissions, PendingQuestion } from './types.ts';
4
5
  interface RestDeps {
5
- sql: any;
6
+ sql: SqlClient;
6
7
  model: LanguageModel;
7
8
  workspace: string;
8
9
  systemPrompt?: string;
@@ -1,9 +1,9 @@
1
- import type { WebSocket } from '../vendor.ts';
1
+ import type { WebSocket, SqlClient } from '../vendor.ts';
2
2
  import type { LanguageModel } from 'ai';
3
3
  import type { Context } from '../types.ts';
4
4
  import type { PendingQuestion, SkillDef, SkillRegistry, OpencodePermissions } from './types.ts';
5
5
  interface WsDeps {
6
- sql: any;
6
+ sql: SqlClient;
7
7
  model: LanguageModel;
8
8
  workspace: string;
9
9
  systemPrompt?: string;
@@ -43,7 +43,6 @@ export interface Queue extends Middleware<Context, Context & QueueInjected>, Clo
43
43
  }): Promise<string>;
44
44
  process<T>(type: string, handler: (job: QueueJob<T>) => Promise<void>): void;
45
45
  run(): Promise<void>;
46
- stop(): void;
47
46
  stats(): {
48
47
  running: boolean;
49
48
  inflight: number;
@@ -1,5 +1,5 @@
1
1
  import type { Redis } from './vendor.ts';
2
- import type { Context, Middleware } from './types.ts';
2
+ import type { Context, Middleware, Closeable } from './types.ts';
3
3
  /** Options for {@link rateLimit}. */
4
4
  export interface RateLimitOptions {
5
5
  /** Maximum requests within the window (default: 100). */
@@ -17,6 +17,14 @@ export interface RateLimitOptions {
17
17
  /** Redis key prefix (default: `'ratelimit:'`). */
18
18
  prefix?: string;
19
19
  }
20
+ /** Rate limit module — middleware + stats. */
21
+ export interface RateLimitModule extends Middleware<Context, Context>, Closeable {
22
+ stats(): {
23
+ store: string;
24
+ entries?: number;
25
+ maxEntries: number;
26
+ };
27
+ }
20
28
  /**
21
29
  * Rate limiting middleware (in-memory or Redis-backed).
22
30
  *
@@ -34,7 +42,4 @@ export interface RateLimitOptions {
34
42
  * app.use(rateLimit({ store: 'redis', redis: new Redis(), max: 100 }))
35
43
  * ```
36
44
  */
37
- export declare function rateLimit(options?: RateLimitOptions): Middleware<Context, Context> & {
38
- close: () => void;
39
- stop?: () => void;
40
- };
45
+ export declare function rateLimit(options?: RateLimitOptions): RateLimitModule;
@@ -0,0 +1,2 @@
1
+ import type { RedisOptions, RedisClient } from './types.ts';
2
+ export declare function redis(opts?: string | RedisOptions): RedisClient;
@@ -1,3 +1,2 @@
1
- import type { RedisOptions, RedisClient } from './types.ts';
2
- export { redis as default };
3
- export declare function redis(opts?: string | RedisOptions): RedisClient;
1
+ export { redis } from './client.ts';
2
+ export type { RedisOptions, RedisClient, RedisInjected } from './types.ts';
@@ -5,6 +5,10 @@ declare module './types.ts' {
5
5
  }
6
6
  }
7
7
  /** Options for {@link requestId}. */
8
+ /** Request ID module — a {@link Middleware} that injects `ctx.requestId`. */
9
+ export type RequestIdModule = Middleware<Context, Context & {
10
+ requestId: string;
11
+ }>;
8
12
  export interface RequestIdOptions {
9
13
  /** Header name for request ID (default: `'X-Request-ID'`). */
10
14
  header?: string;
package/dist/router.d.ts CHANGED
@@ -10,24 +10,6 @@ export type WebSocketHandler = {
10
10
  error?: (ws: WebSocket, ctx: Context, error: Error) => void | Promise<void>;
11
11
  };
12
12
  type WsUpgradeHandler = (req: IncomingMessage, socket: Duplex, head: Buffer) => void;
13
- /**
14
- * Middleware metadata for dependency checking.
15
- * Middleware factories can attach this to their return value for runtime validation.
16
- *
17
- * ```ts
18
- * function postgres(): PostgresClient {
19
- * const mw = async (req, ctx, next) => { ... }
20
- * mw.__meta = { injects: ['sql'], depends: [] }
21
- * return Object.assign(mw, { sql, migrate, close })
22
- * }
23
- * ```
24
- */
25
- export interface MiddlewareMeta {
26
- /** Fields this middleware injects into ctx. */
27
- injects: string[];
28
- /** Fields this middleware depends on (must be injected earlier). */
29
- depends: string[];
30
- }
31
13
  export declare class Router<T extends Context = Context> {
32
14
  private root;
33
15
  private wsRoot;
package/dist/serve.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { type IncomingMessage, type ServerResponse } from 'node:http';
2
2
  import type { Duplex } from 'node:stream';
3
- import type { Handler } from './types.ts';
3
+ import { type Handler } from './types.ts';
4
4
  export interface ServeOptions {
5
5
  port?: number;
6
6
  hostname?: string;
@@ -18,6 +18,8 @@ export interface ServeOptions {
18
18
  }
19
19
  export interface Server {
20
20
  stop: (timeoutMs?: number) => Promise<void>;
21
+ /** Alias for `stop()`. Prefer this for consistency with other modules. */
22
+ close: (timeoutMs?: number) => Promise<void>;
21
23
  readonly port: number;
22
24
  readonly hostname: string;
23
25
  ready: Promise<void>;
@@ -8,5 +8,3 @@ export declare function getServerModule(absPath: string): any;
8
8
  * Otherwise clear all modules.
9
9
  */
10
10
  export declare function clearServerModule(absPath?: string): void;
11
- /** Release resources. Call when shutting down. */
12
- export declare function closeRegistry(): void;
package/dist/session.d.ts CHANGED
@@ -80,6 +80,10 @@ export declare class MemoryStore implements SessionStore {
80
80
  /** Testing only: return approximate count. */
81
81
  get size(): number;
82
82
  }
83
+ /**
84
+ * Redis-backed session store.
85
+ * Pass to `session({ store: new RedisStore({ redis }) })`.
86
+ */
83
87
  export declare class RedisStore implements SessionStore {
84
88
  private redis;
85
89
  private prefix;
@@ -90,6 +94,22 @@ export declare class RedisStore implements SessionStore {
90
94
  destroy(sid: string): Promise<void>;
91
95
  close(): Promise<void>;
92
96
  }
97
+ /**
98
+ * Session middleware. Injects `ctx.session` with a persistent key-value store
99
+ * scoped to the request. Data is automatically saved to the store on response.
100
+ *
101
+ * Defaults to memory store. Use `{ store: 'redis', redis }` for multi-process setups.
102
+ *
103
+ * ```ts
104
+ * import { session } from 'weifuwu'
105
+ * app.use(session())
106
+ *
107
+ * app.get('/visit', (req, ctx) => {
108
+ * ctx.session.count = (ctx.session.count ?? 0) + 1
109
+ * return Response.json({ visits: ctx.session.count })
110
+ * })
111
+ * ```
112
+ */
93
113
  export declare function session(options?: SessionOptions): Middleware<Context, Context & SessionInjected> & {
94
114
  close: () => Promise<void>;
95
115
  store: SessionStore;
package/dist/ssr.d.ts CHANGED
@@ -1,13 +1,4 @@
1
1
  import { Router } from './router.ts';
2
- interface ResolvedRoute {
3
- routePath: string;
4
- pageFile: string;
5
- layoutFiles: string[];
6
- errorFiles: string[];
7
- notFoundFile: string | null;
8
- }
9
- /** Clear route cache (called by HMR watcher in dev mode). */
10
- export declare function clearRouteCache(cache: Map<string, ResolvedRoute | null>): void;
11
2
  export interface RouteEntry {
12
3
  path: string;
13
4
  file: string;
@@ -18,4 +9,3 @@ export declare function ssr(opts: {
18
9
  close?: () => void;
19
10
  pages?: () => RouteEntry[];
20
11
  };
21
- export {};
@@ -5,6 +5,5 @@ export declare function sqlTypeForField(field: FieldDef): string;
5
5
  export declare function validateSlug(slug: string): string | null;
6
6
  export declare function validateFieldDefs(fields: FieldDef[]): string[];
7
7
  export declare function formatDefault(field: FieldDef): string;
8
- export declare function mapFieldToZod(field: FieldDef): string;
9
8
  export declare function getRelationFields(fields: FieldDef[]): FieldDef[];
10
9
  export declare function findRelation(fields: FieldDef[], targetSlug: string): FieldDef | undefined;
package/dist/types.d.ts CHANGED
@@ -4,12 +4,25 @@ export interface Context {
4
4
  mountPath?: string;
5
5
  layoutStack?: {
6
6
  path: string;
7
- component: any;
7
+ component: unknown;
8
8
  }[];
9
9
  [key: string]: unknown;
10
10
  }
11
11
  export type Handler<T extends Context = Context> = (req: Request, ctx: T) => Response | Promise<Response>;
12
- export type Middleware<In extends Context = Context, Out extends In = In> = (req: Request, ctx: In, next: Handler<Out>) => Response | Promise<Response>;
12
+ /**
13
+ * Metadata for middleware dependency checking.
14
+ * Middleware factories attach this for runtime validation.
15
+ */
16
+ export interface MiddlewareMeta {
17
+ /** Fields this middleware injects into ctx. */
18
+ injects: string[];
19
+ /** Fields this middleware depends on (must be injected earlier). */
20
+ depends: string[];
21
+ }
22
+ export type Middleware<In extends Context = Context, Out extends In = In> = {
23
+ (req: Request, ctx: In, next: Handler<Out>): Response | Promise<Response>;
24
+ __meta?: MiddlewareMeta;
25
+ };
13
26
  export type ErrorHandler<T extends Context = Context> = (error: Error, req: Request, ctx: T) => Response | Promise<Response>;
14
27
  /**
15
28
  * Interface for resources that require explicit cleanup (connections, pools, timers).
@@ -19,3 +32,16 @@ export interface Closeable {
19
32
  /** Release all resources. Call once when shutting down. */
20
33
  close(): Promise<void>;
21
34
  }
35
+ /**
36
+ * HTTP error with an explicit status code.
37
+ * Throw from a handler or middleware to return a non-200 response.
38
+ *
39
+ * ```ts
40
+ * if (!resource) throw new HttpError('Not found', 404)
41
+ * serve() catches it and returns the status code.
42
+ * ```
43
+ */
44
+ export declare class HttpError extends Error {
45
+ status: number;
46
+ constructor(message: string, status: number);
47
+ }
package/dist/upload.d.ts CHANGED
@@ -4,6 +4,10 @@ declare module './types.ts' {
4
4
  parsed: Record<string, unknown>;
5
5
  }
6
6
  }
7
+ /** Upload middleware — a {@link Middleware} that injects `ctx.parsed` with file fields. */
8
+ export type UploadModule = Middleware<Context, Context & {
9
+ parsed: Record<string, unknown>;
10
+ }>;
7
11
  /** A parsed file from a multipart upload. */
8
12
  export interface UploadedFile {
9
13
  /** Original filename from the client. */
@@ -1,4 +1,9 @@
1
- import type { UserOptions, UserModule } from './types.ts';
1
+ import type { UserOptions, UserData, UserModule } from './types.ts';
2
+ declare module '../types.ts' {
3
+ interface Context {
4
+ user: UserData;
5
+ }
6
+ }
2
7
  /**
3
8
  * User authentication module — local register/login, JWT verification, OAuth2 server, social login.
4
9
  * Supports DB-less auth via tokens/verify/proxy options.
@@ -9,13 +9,13 @@ interface OAuthLoginDeps {
9
9
  /** Table for provider-user link, derived from usersTable. */
10
10
  providerTable: string;
11
11
  redirectUrl: string;
12
- signToken: (user: any) => string;
12
+ signToken: (user: Record<string, unknown>) => string;
13
13
  /** Create a placeholder user for OAuth login (no password). */
14
- createPlaceholderUser: (email: string, name: string) => Promise<any>;
14
+ createPlaceholderUser: (email: string, name: string) => Promise<Record<string, unknown>>;
15
15
  /** Find user by internal ID. */
16
- findUserById: (id: number) => Promise<any | undefined>;
16
+ findUserById: (id: number) => Promise<Record<string, unknown> | undefined>;
17
17
  /** Find user by email. */
18
- findUserByEmail: (email: string) => Promise<any | undefined>;
18
+ findUserByEmail: (email: string) => Promise<Record<string, unknown> | undefined>;
19
19
  }
20
20
  export declare function registerOAuthLoginRoutes(router: Router, deps: OAuthLoginDeps, providers: Record<string, OAuthProviderConfig>): void;
21
21
  export {};
@@ -5,10 +5,28 @@ declare module './types.ts' {
5
5
  parsed: Record<string, unknown>;
6
6
  }
7
7
  }
8
+ /** Validation middleware — a {@link Middleware} that injects `ctx.parsed` with validated data. */
9
+ export type ValidateModule = Middleware;
8
10
  export interface ValidationSchemas {
9
11
  body?: ZodSchema;
10
12
  query?: ZodSchema;
11
13
  params?: ZodSchema;
12
14
  headers?: ZodSchema;
13
15
  }
16
+ /**
17
+ * Request validation middleware using Zod schemas.
18
+ *
19
+ * Validates `params`, `query`, `body`, and/or `headers` against schemas.
20
+ * Returns 422 with error details on mismatch.
21
+ * Injects `ctx.parsed` with validated-and-transformed values.
22
+ *
23
+ * ```ts
24
+ * import { z } from 'zod'
25
+ *
26
+ * app.get('/users/:id', validate({
27
+ * params: z.object({ id: z.string() }),
28
+ * query: z.object({ include: z.string().optional() }),
29
+ * }), handler)
30
+ * ```
31
+ */
14
32
  export declare function validate(schemas?: ValidationSchemas): Middleware;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.25.0",
4
+ "version": "0.25.2",
5
5
  "description": "Web-standard HTTP framework for Node.js — (req, ctx) => Response",
6
6
  "main": "./dist/index.js",
7
7
  "types": "./dist/index.d.ts",
@@ -31,8 +31,9 @@
31
31
  "lint": "eslint --ext .ts,.tsx .",
32
32
  "test": "node --test 'test/**/*.test.ts'",
33
33
  "test:coverage": "node --experimental-test-coverage --test 'test/**/*.test.ts'",
34
- "test:unit": "bash scripts/test-unit.sh",
35
- "test:ci": "node --test 'test/**/*.test.ts'",
34
+ "test:typecheck": "npx tsc -p tsconfig.test.json --noEmit",
35
+ "test:quick": "bash scripts/test-quick.sh",
36
+ "test:ci": "node --test --test-force-exit --test-timeout=60000 'test/**/*.test.ts'",
36
37
  "prepare": "husky"
37
38
  },
38
39
  "dependencies": {