weifuwu 0.56.0 → 0.57.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * weifuwu/rateLimit — 限流中间件
3
+ *
4
+ * 注入 ctx.limit(手动限流)+ 全局请求限流。
5
+ *
6
+ * store:
7
+ * - redis(默认):fixed(INCR + EXPIRE,原子无竞态)或 sliding(ZSET 时间戳),
8
+ * 计数在 redis 内 → 多实例天然一致
9
+ * - memory:进程内 Map,仅单实例/开发环境(多实例会互相放行,文档红线)
10
+ *
11
+ * 裁剪声明:
12
+ * ✅ fixed / sliding / redis / memory / 响应头 / ctx.limit / 自定义 key
13
+ * ❌ 多级限流 DSL、配额策略引擎、动态规则热更新、按租户配额(应用层)
14
+ *
15
+ * ```ts
16
+ * import { rateLimit } from 'weifuwu'
17
+ *
18
+ * app.use(redis())
19
+ * app.use(rateLimit({ windowMs: 60_000, max: 100 })) // 全局限流
20
+ *
21
+ * app.get('/api/search', async (req, ctx) => {
22
+ * await ctx.limit('search', { max: 30, windowMs: 60_000 }) // 手动限流
23
+ * ...
24
+ * })
25
+ * ```
26
+ */
27
+ import type { Context, Middleware } from '../types.ts';
28
+ export type RateLimitAlgorithm = 'fixed' | 'sliding';
29
+ export type RateLimitStore = 'redis' | 'memory';
30
+ export interface RateLimitOptions {
31
+ /** 时间窗口(ms)。默认 60_000。 */
32
+ windowMs?: number;
33
+ /** 窗口内最大请求数。默认 100。 */
34
+ max?: number;
35
+ /** 限流键(默认取 X-Forwarded-For 首个 IP;生产环境请配置反向代理注入该头)。 */
36
+ key?: (req: Request) => string;
37
+ /** 算法。'fixed'(默认)| 'sliding'(sliding 仅支持 redis store) */
38
+ algorithm?: RateLimitAlgorithm;
39
+ /** 存储。'redis'(默认)| 'memory'(单实例/开发) */
40
+ store?: RateLimitStore;
41
+ /** 超限状态码。默认 429。 */
42
+ statusCode?: number;
43
+ /** 超限错误消息。默认 'Too Many Requests'。 */
44
+ message?: string;
45
+ /** 是否输出 RateLimit-* / Retry-After 响应头。默认 true。 */
46
+ headers?: boolean;
47
+ }
48
+ /** ctx.limit 手动限流注入 */
49
+ export interface RateLimitInjected {
50
+ /**
51
+ * 手动限流(handler 内对特定资源限流)。
52
+ * 超限抛 HttpError(默认 429)。
53
+ *
54
+ * ```ts
55
+ * await ctx.limit('search', { max: 30, windowMs: 60_000 })
56
+ * ```
57
+ */
58
+ limit: (name: string, opts?: {
59
+ max?: number;
60
+ windowMs?: number;
61
+ }) => Promise<void>;
62
+ }
63
+ declare module '../types.ts' {
64
+ interface Context {
65
+ limit?: RateLimitInjected['limit'];
66
+ }
67
+ }
68
+ export interface RateLimitClient extends Middleware<Context, Context & RateLimitInjected> {
69
+ }
70
+ export declare function rateLimit(options?: RateLimitOptions): RateLimitClient;
@@ -0,0 +1,90 @@
1
+ /**
2
+ * weifuwu/queue — 可靠任务队列(Redis Streams 消费组)
3
+ *
4
+ * 语义(文档红线):
5
+ * - at-least-once:handler 可能重复执行(崩溃/超时重投)——幂等由业务保证
6
+ * - visibility timeout:处理失败不 XACK → 超时后被 XAUTOCLAIM 重新投递
7
+ * - attempts 用尽 → DLQ(q:{name}:dead)
8
+ * - 多 worker 实例安全:消费组内 entry 只被一个 consumer 处理
9
+ *
10
+ * 诚实裁剪:
11
+ * ✅ add / worker(concurrency / visibilityTimeout)/ 重试(固定间隔 =
12
+ * visibilityTimeout)/ DLQ / length
13
+ * ❌ 延迟调度、cron、优先级、指数退避、速率限制(间隔固定为
14
+ * visibilityTimeout)、流式进度(应用层实现)
15
+ *
16
+ * ```ts
17
+ * import { queue } from 'weifuwu'
18
+ *
19
+ * const q = queue() // 默认 REDIS_URL
20
+ * app.use(q) // 注入 ctx.queue(handler 内 add)
21
+ *
22
+ * app.post('/api/jobs', async (req, ctx) => {
23
+ * await ctx.queue.add('llm.batch', { prompt: '...' }, { attempts: 3 })
24
+ * return accepted()
25
+ * })
26
+ *
27
+ * const worker = q.worker('llm.batch', async (job) => {
28
+ * await runLLM(job.data) // 失败 → 自动重试 → 用尽进 DLQ
29
+ * }, { concurrency: 5, visibilityTimeout: 30_000 })
30
+ * await worker.start()
31
+ * ```
32
+ */
33
+ import type { Context, Middleware } from '../types.ts';
34
+ export interface QueueOptions {
35
+ /** Redis 连接串(默认 REDIS_URL) */
36
+ url?: string;
37
+ /** stream 名前缀。默认 'q:'。 */
38
+ prefix?: string;
39
+ poolSize?: number;
40
+ }
41
+ export interface AddOptions {
42
+ /** 最大尝试次数(含首次)。默认 3。 */
43
+ attempts?: number;
44
+ }
45
+ export interface Job<T = unknown> {
46
+ /** stream entry id */
47
+ id: string;
48
+ data: T;
49
+ /** 已尝试次数 */
50
+ attempts: number;
51
+ name: string;
52
+ }
53
+ export interface WorkerOptions {
54
+ /** 每次 XREADGROUP 批量拉取数(≈并发度)。默认 1。 */
55
+ concurrency?: number;
56
+ /** 失败后重新投递间隔(ms)。默认 30_000。 */
57
+ visibilityTimeout?: number;
58
+ /** 消费组 consumer 名(多实例自动 hostname-pid,可覆盖) */
59
+ consumer?: string;
60
+ }
61
+ export interface QueueWorker {
62
+ /** 启动消费循环(阻塞直到 stop) */
63
+ start: () => Promise<void>;
64
+ /** 优雅停止:停止拉取,等待 in-flight 完成 */
65
+ stop: () => Promise<void>;
66
+ }
67
+ export interface QueueClient {
68
+ /** 入队(生产者) */
69
+ add: (name: string, data: unknown, opts?: AddOptions) => Promise<{
70
+ id: string;
71
+ }>;
72
+ /** 创建 worker(消费者)。worker 独立于请求生命周期,启动时创建。 */
73
+ worker: <T = unknown>(name: string, handler: (job: Job<T>) => Promise<void>, opts?: WorkerOptions) => QueueWorker;
74
+ /** 队列当前累积长度(XLEN)。注意:XACK 不删除 stream entry,
75
+ * 消费后 length 不归零;待处理/进行中数量用 XPENDING(ctx.redis.command)。 */
76
+ length: (name: string) => Promise<number>;
77
+ }
78
+ export interface QueueInjected {
79
+ queue: QueueClient;
80
+ }
81
+ declare module '../types.ts' {
82
+ interface Context {
83
+ queue?: QueueClient;
84
+ }
85
+ }
86
+ export interface QueueClientModule extends Middleware<Context, Context & QueueInjected> {
87
+ queue: QueueClient;
88
+ close: () => Promise<void>;
89
+ }
90
+ export declare function queue(options?: QueueOptions): QueueClientModule;
package/dist/types.d.ts CHANGED
@@ -28,8 +28,8 @@ export interface Context {
28
28
  params: Record<string, string>;
29
29
  query: Record<string, string>;
30
30
  mountPath?: string;
31
- /** Currently authenticated user (set by user() or custom auth middleware). */
32
- user?: unknown;
31
+ /** Currently authenticated user (set by user() or userSystem()). */
32
+ user?: User | null;
33
33
  loaderData?: Record<string, unknown>;
34
34
  /** Public environment variables. */
35
35
  env?: Record<string, string>;
@@ -0,0 +1,97 @@
1
+ /**
2
+ * weifuwu/userSystem — 用户系统中间件
3
+ *
4
+ * 注入 ctx.user(从 Authorization: Bearer 解码 + 查库)+ ctx.auth(方法面)。
5
+ * users.routes(app) 注册 /api/auth/* 路由;users.migrate() 幂等建表。
6
+ *
7
+ * 安全基线:
8
+ * - scrypt 密码哈希(per-user salt + timing-safe compare,异步不阻塞)
9
+ * - access token:HMAC-SHA256 JWT(无状态,与 client auth() 兼容)
10
+ * - refresh token:不透明随机串,DB 只存哈希,logout/轮换即撤销
11
+ * - 登录失败统一 401(不泄露"邮箱是否存在"——防枚举)
12
+ *
13
+ * 诚实裁剪:
14
+ * ✅ register/login/logout/me/refresh / setPassword / createToken / tenant claim
15
+ * ❌ OAuth、邮箱验证邮件(给 createToken + setPassword 底层 API 自接)、
16
+ * 多因素、RBAC 权限引擎(只提供 role 字段)、多租户语义(tenant-ready)
17
+ *
18
+ * ```ts
19
+ * const db = postgres()
20
+ * await db.migrate()
21
+ * const users = userSystem({ sql: db.sql, secret: process.env.AUTH_SECRET! })
22
+ * await users.migrate()
23
+ * app.use(db)
24
+ * app.use(users)
25
+ * users.routes(app)
26
+ *
27
+ * app.get('/me', (req, ctx) => ok(ctx.user)) // ctx.user 已注入
28
+ * app.post('/secure', (req, ctx) => {
29
+ * ctx.auth.requireAuth() // 未登录抛 401
30
+ * })
31
+ * ```
32
+ */
33
+ import type { Context, Middleware } from '../types.ts';
34
+ import { type User } from '../types.ts';
35
+ import type { Router } from '../core/router.ts';
36
+ import type { SqlClient } from '../postgres/types.ts';
37
+ export interface UserSystemOptions {
38
+ /** PostgreSQL SqlClient(postgres() 中间件的 .sql) */
39
+ sql: SqlClient;
40
+ /** HMAC 签名密钥(至少 32 字符;默认读 AUTH_SECRET) */
41
+ secret?: string;
42
+ /** access token 有效期(秒)。默认 3600(1h)。 */
43
+ accessTtlSeconds?: number;
44
+ /** refresh token 有效期(天)。默认 30。 */
45
+ refreshTtlDays?: number;
46
+ /** 路由前缀。默认 '/api/auth'。 */
47
+ prefix?: string;
48
+ }
49
+ export interface RegisterInput {
50
+ email: string;
51
+ password: string;
52
+ name?: string;
53
+ role?: string;
54
+ tenant?: string;
55
+ }
56
+ /** ctx.auth 方法面 */
57
+ export interface AuthApi {
58
+ register(input: RegisterInput): Promise<{
59
+ token: string;
60
+ refreshToken: string;
61
+ user: User;
62
+ }>;
63
+ login(email: string, password: string): Promise<{
64
+ token: string;
65
+ refreshToken: string;
66
+ user: User;
67
+ }>;
68
+ /** 撤销 refresh token(logout) */
69
+ logout(refreshToken: string): Promise<void>;
70
+ /** 未登录抛 HttpError(401),否则返回当前用户 */
71
+ requireAuth(): User;
72
+ /** 修改密码(登录态;旧密码失效) */
73
+ setPassword(userId: string, newPassword: string): Promise<void>;
74
+ /** 生成业务 token(邮箱验证/密码重置自接——应用层用它发邮件) */
75
+ createToken(type: string, payload: Record<string, unknown>, opts: {
76
+ ttlSeconds: number;
77
+ }): string;
78
+ }
79
+ export interface UserInjected {
80
+ /** 当前用户(未登录 = null)。每次请求从 token 解码 + 查库(删号/角色变更即时生效) */
81
+ user: User | null;
82
+ auth: AuthApi;
83
+ }
84
+ declare module '../types.ts' {
85
+ interface Context {
86
+ auth?: AuthApi;
87
+ }
88
+ }
89
+ export interface UserSystemClient extends Middleware<Context, Context & UserInjected> {
90
+ /** 幂等建表(users + sessions) */
91
+ migrate: () => Promise<void>;
92
+ /** 注册 /api/auth/* 路由(register/login/logout/me/refresh) */
93
+ routes: (app: Router, opts?: {
94
+ prefix?: string;
95
+ }) => void;
96
+ }
97
+ export declare function userSystem(options: UserSystemOptions): UserSystemClient;
@@ -0,0 +1,8 @@
1
+ /**
2
+ * 密码哈希(node:crypto scrypt,异步版不阻塞事件循环)
3
+ *
4
+ * 格式:`scrypt$N$r$p$salt$hash`(参数进哈希串 → 未来可升级参数,旧哈希可验证)
5
+ * per-user 随机 salt + timingSafeEqual 比较(防 timing 攻击)
6
+ */
7
+ export declare function hashPassword(password: string): Promise<string>;
8
+ export declare function verifyPassword(password: string, stored: string): Promise<boolean>;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * 会话令牌(零依赖,node:crypto)
3
+ *
4
+ * access token:HMAC-SHA256 签名的 JWT 形态(base64url),
5
+ * 与 weifuwu/client 的 auth() 兼容(客户端解码 payload 检查 exp)。
6
+ * refresh token:不透明随机串(256-bit),DB 只存 SHA-256 哈希(可撤销)。
7
+ */
8
+ /** 签发 JWT access token(HS256) */
9
+ export declare function signToken(payload: Record<string, unknown>, secret: string, ttlSeconds: number): string;
10
+ /** 验证 JWT(签名 + 过期)。失败返回 null(不是抛错——未登录是正常状态) */
11
+ export declare function verifyToken(token: string, secret: string): Record<string, unknown> | null;
12
+ /** 不透明 refresh token(256-bit 随机) */
13
+ export declare function generateRefreshToken(): string;
14
+ /** DB 只存哈希(泄露库也不泄露 refresh token) */
15
+ export declare function hashRefreshToken(token: string): string;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "weifuwu",
3
3
  "type": "module",
4
- "version": "0.56.0",
4
+ "version": "0.57.0",
5
5
  "description": "AI SaaS framework — (req, ctx) => Response",
6
6
  "exports": {
7
7
  ".": {