weifuwu 0.56.1 → 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.
- package/README.md +127 -0
- package/dist/core/router.d.ts +1 -1
- package/dist/email/index.d.ts +72 -0
- package/dist/email/smtp.d.ts +34 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +2745 -1975
- package/dist/middleware/rate-limit.d.ts +70 -0
- package/dist/queue/index.d.ts +90 -0
- package/dist/types.d.ts +2 -2
- package/dist/user/index.d.ts +97 -0
- package/dist/user/password.d.ts +8 -0
- package/dist/user/token.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -230,6 +230,10 @@ createApp().use(router({ routes })).mount('#root', RouteView, { hydrate: true })
|
|
|
230
230
|
| `weifuwu` | **redis** | Redis 客户端(自研 RESP2 协议)→ `ctx.redis` | Router, REDIS_URL |
|
|
231
231
|
| `weifuwu` | **ui** | SSR 渲染 + esbuild JS/CSS 动态编译 → `ctx.ui` | Router |
|
|
232
232
|
| `weifuwu` | **uiSsr** | 路由级 SSR:匹配 routes → 自动完整 HTML + `__DATA__` + bundle | Router, ui |
|
|
233
|
+
| `weifuwu` | **rateLimit** | 限流中间件(fixed/sliding,redis 多实例原子)→ `ctx.limit` | Router, redis |
|
|
234
|
+
| `weifuwu` | **email** | 邮件发送(Resend/SMTP 自研/自定义适配器)→ `ctx.email` | Router |
|
|
235
|
+
| `weifuwu` | **userSystem** | 用户系统(scrypt 密码哈希 + 混合会话)→ `ctx.user` / `ctx.auth` + `/api/auth/*` | Router, postgres |
|
|
236
|
+
| `weifuwu` | **queue** | 可靠任务队列(Redis Streams,at-least-once + DLQ)→ `ctx.queue` | Router, redis |
|
|
233
237
|
| `weifuwu/dev` | **dev loader** | Node loader:服务端直接跑 `.ts/.tsx`(`--import weifuwu/dev`) | esbuild |
|
|
234
238
|
| `weifuwu` | **graphql** | GraphQL 端点(支持 GraphiQL) | Router |
|
|
235
239
|
| `weifuwu` | **createMiddleware** | 类型安全中间件工厂 | — |
|
|
@@ -1019,6 +1023,8 @@ app.get('/secure', () => {
|
|
|
1019
1023
|
|
|
1020
1024
|
## 响应辅助函数
|
|
1021
1025
|
|
|
1026
|
+
> 以下为完整 API 参考,按需查阅。四个 SaaS 地基模块(rateLimit / email / userSystem / queue)见文末「SaaS 地基模块」章节。
|
|
1027
|
+
|
|
1022
1028
|
消除 `Response.json(...)` 重复模式:
|
|
1023
1029
|
|
|
1024
1030
|
```ts
|
|
@@ -2722,3 +2728,124 @@ node scripts/release.mjs <version> # 发布
|
|
|
2722
2728
|
# 测试前启动依赖服务
|
|
2723
2729
|
docker compose up -d
|
|
2724
2730
|
```
|
|
2731
|
+
|
|
2732
|
+
---
|
|
2733
|
+
|
|
2734
|
+
# SaaS 地基模块(rateLimit / email / userSystem / queue)
|
|
2735
|
+
|
|
2736
|
+
四个内建模块组成一个"基本 SaaS 底座":认证、异步任务、限流、邮件——零新增依赖
|
|
2737
|
+
(只依赖已自研的 redis / postgres 客户端与 node 标准库)。
|
|
2738
|
+
|
|
2739
|
+
## rateLimit — 限流
|
|
2740
|
+
|
|
2741
|
+
```ts
|
|
2742
|
+
import { rateLimit } from 'weifuwu'
|
|
2743
|
+
|
|
2744
|
+
app.use(redis()) // 依赖 ctx.redis
|
|
2745
|
+
app.use(rateLimit({ windowMs: 60_000, max: 100 })) // 全局限流(默认固定窗口)
|
|
2746
|
+
|
|
2747
|
+
app.get('/api/search', async (req, ctx) => {
|
|
2748
|
+
await ctx.limit('search', { max: 30, windowMs: 60_000 }) // 手动限流,超限抛 429
|
|
2749
|
+
})
|
|
2750
|
+
|
|
2751
|
+
// 登录防爆破(配合 userSystem):组合键 ip:email
|
|
2752
|
+
app.use(rateLimit({ key: (req) => `login:${req.ip}:${req.email}`, max: 5, windowMs: 15 * 60_000 }))
|
|
2753
|
+
```
|
|
2754
|
+
|
|
2755
|
+
| 选项 | 默认 | 说明 |
|
|
2756
|
+
|------|------|------|
|
|
2757
|
+
| `windowMs` | `60000` | 时间窗口 |
|
|
2758
|
+
| `max` | `100` | 窗口内最大请求 |
|
|
2759
|
+
| `key` | X-Forwarded-For | 限流键(生产环境配置反向代理注入) |
|
|
2760
|
+
| `algorithm` | `fixed` | `fixed`(INCR+EXPIRE,原子)\| `sliding`(ZSET,仅 redis) |
|
|
2761
|
+
| `store` | `redis` | `redis`(多实例一致)\| `memory`(仅单实例/开发) |
|
|
2762
|
+
|
|
2763
|
+
- 响应自动带 `RateLimit-Limit` / `RateLimit-Remaining` / `RateLimit-Reset` / `Retry-After`
|
|
2764
|
+
- 多实例共享计数:计数在 redis,水平扩展天然一致
|
|
2765
|
+
|
|
2766
|
+
## email — 邮件发送
|
|
2767
|
+
|
|
2768
|
+
```ts
|
|
2769
|
+
import { email } from 'weifuwu'
|
|
2770
|
+
|
|
2771
|
+
app.use(email({ from: 'no-reply@your.app', adapter: 'resend', resend: { apiKey: process.env.RESEND_API_KEY } }))
|
|
2772
|
+
// 或 adapter: 'smtp' + smtp: { host, port, user, pass }(自研 SMTP 客户端,零依赖)
|
|
2773
|
+
|
|
2774
|
+
app.post('/api/notify', async (req, ctx) => {
|
|
2775
|
+
await ctx.email.send({ to: 'user@x.com', subject: '通知', html: '<h1>hi</h1>' })
|
|
2776
|
+
})
|
|
2777
|
+
```
|
|
2778
|
+
|
|
2779
|
+
- 适配器:`resend`(默认,一个 POST)/ `smtp`(自研 node:net + node:tls:EHLO/STARTTLS/AUTH PLAIN/DATA/dot-stuffing,非 ASCII subject 自动 RFC2047 编码)/ 自定义函数
|
|
2780
|
+
- 裁剪:附件、退信/送达率(服务商职责)、批量营销不支持
|
|
2781
|
+
|
|
2782
|
+
## userSystem — 用户系统
|
|
2783
|
+
|
|
2784
|
+
```ts
|
|
2785
|
+
import { userSystem } from 'weifuwu'
|
|
2786
|
+
|
|
2787
|
+
const db = postgres()
|
|
2788
|
+
await db.migrate()
|
|
2789
|
+
const users = userSystem({ sql: db.sql, secret: process.env.AUTH_SECRET })
|
|
2790
|
+
await users.migrate() // 幂等建表(users + sessions)
|
|
2791
|
+
app.use(db)
|
|
2792
|
+
app.use(users) // 注入 ctx.user / ctx.auth
|
|
2793
|
+
users.routes(app) // POST /api/auth/register|login|logout|refresh + GET /api/auth/me
|
|
2794
|
+
|
|
2795
|
+
app.get('/me', (req, ctx) => ok(ctx.user)) // 已注入
|
|
2796
|
+
app.post('/secure', (req, ctx) => { ctx.auth.requireAuth(); ... })
|
|
2797
|
+
```
|
|
2798
|
+
|
|
2799
|
+
- **安全基线**:scrypt 密码哈希(per-user salt + timing-safe,异步不阻塞);access token = HMAC-SHA256 JWT(与 `weifuwu/client` 的 `auth()` 天然配对);refresh token = 不透明随机串,DB 只存哈希,logout/轮换即撤销
|
|
2800
|
+
- **防枚举**:登录失败统一 401(不泄露邮箱是否存在)
|
|
2801
|
+
- **`ctx.auth` 方法面**:`register` / `login` / `logout` / `requireAuth` / `setPassword(userId, newPwd)` / `createToken(type, payload, { ttlSeconds })`(邮箱验证/密码重置自接)
|
|
2802
|
+
- **裁剪**:OAuth、邮箱验证邮件(给底层 API 自接)、多因素、RBAC 权限引擎(只留 `role` 字段)、多租户语义(tenant-ready:`tenant` 字段 + token claim 已预留)
|
|
2803
|
+
|
|
2804
|
+
## queue — 可靠任务队列
|
|
2805
|
+
|
|
2806
|
+
```ts
|
|
2807
|
+
import { queue } from 'weifuwu'
|
|
2808
|
+
|
|
2809
|
+
const q = queue() // 默认 REDIS_URL
|
|
2810
|
+
app.use(q) // 注入 ctx.queue
|
|
2811
|
+
|
|
2812
|
+
app.post('/api/generate', async (req, ctx) => {
|
|
2813
|
+
await ctx.queue.add('llm.batch', { prompt: '...' }, { attempts: 3 })
|
|
2814
|
+
return new Response(null, { status: 202 }) // 立即 202,任务后台执行
|
|
2815
|
+
})
|
|
2816
|
+
|
|
2817
|
+
// 消费者(独立进程或同进程均可,多开安全)
|
|
2818
|
+
const worker = q.worker('llm.batch', async (job) => {
|
|
2819
|
+
await runLLM(job.data) // 失败自动重试 → 用尽进 DLQ(q:llm.batch:dead)
|
|
2820
|
+
}, { concurrency: 5, visibilityTimeout: 30_000 })
|
|
2821
|
+
await worker.start()
|
|
2822
|
+
await worker.stop() // 优雅停止
|
|
2823
|
+
```
|
|
2824
|
+
|
|
2825
|
+
- **语义**:at-least-once(handler 可能重复执行——幂等由业务保证);Redis Streams 消费组,多 worker 实例不重复消费
|
|
2826
|
+
- **可靠性**:失败 → 延迟重试(间隔 = `visibilityTimeout`,ZSET 延迟队列)→ attempts 用尽 → DLQ;worker 崩溃 → pending 由其他实例 `XAUTOCLAIM` 接管
|
|
2827
|
+
- 裁剪:延迟调度(除重试外)、cron、优先级、指数退避、速率限制不支持
|
|
2828
|
+
|
|
2829
|
+
## 组合示例:注册 → 验证邮件 → 欢迎任务 → 登录防爆破
|
|
2830
|
+
|
|
2831
|
+
```ts
|
|
2832
|
+
app.use(redis())
|
|
2833
|
+
app.use(rateLimit({ key: (req) => `login:${req.ip}`, max: 5, windowMs: 60_000 })) // 防爆破
|
|
2834
|
+
app.use(email({ from: 'no-reply@x.com', adapter: 'resend', resend: { apiKey } }))
|
|
2835
|
+
app.use(db)
|
|
2836
|
+
app.use(users)
|
|
2837
|
+
users.routes(app)
|
|
2838
|
+
|
|
2839
|
+
// 注册:限流守卫 → 用户系统 → 验证邮件 → 欢迎任务入队
|
|
2840
|
+
app.post('/api/auth/register', async (req, ctx) => {
|
|
2841
|
+
await ctx.limit(`register:${req.ip}`, { max: 10, windowMs: 60_000 })
|
|
2842
|
+
const result = await ctx.auth.register(await req.json())
|
|
2843
|
+
const token = ctx.auth.createToken('verify', { sub: result.user.id }, { ttlSeconds: 86400 })
|
|
2844
|
+
await ctx.email.send({ to: result.user.email, subject: '验证邮箱', html: `...?token=${token}` })
|
|
2845
|
+
await ctx.queue.add('welcome.flow', { userId: result.user.id })
|
|
2846
|
+
return created(result)
|
|
2847
|
+
})
|
|
2848
|
+
|
|
2849
|
+
const worker = q.worker('welcome.flow', async (job) => { /* 欢迎流程 */ })
|
|
2850
|
+
await worker.start()
|
|
2851
|
+
```
|
package/dist/core/router.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type Context, type Handler, type Middleware, type ErrorHandler, type Closeable } from '../types.ts';
|
|
2
2
|
import { type WebSocketHandler, type WsUpgradeHandler } from './ws.ts';
|
|
3
3
|
import type { GraphQLHandler } from '../graphql.ts';
|
|
4
4
|
/**
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* weifuwu/email — 邮件发送中间件
|
|
3
|
+
*
|
|
4
|
+
* 注入 ctx.email.send —— 统一发送接口,适配器抽象底层服务商。
|
|
5
|
+
*
|
|
6
|
+
* 适配器:
|
|
7
|
+
* - 'resend'(默认推荐):一个 POST,独立开发者首选
|
|
8
|
+
* - 'smtp':自研 SMTP 客户端(node:net + node:tls,零依赖),任意服务商/自建 Postfix
|
|
9
|
+
* - 自定义函数:`adapter: async (msg) => ...` 一行接入任意服务商
|
|
10
|
+
*
|
|
11
|
+
* 裁剪声明:
|
|
12
|
+
* ✅ 统一 send 接口 / text + html / 多收件人 / 自定义适配器
|
|
13
|
+
* ❌ 附件(SMTP MIME multipart 未实现)、退信/送达率(服务商职责)、
|
|
14
|
+
* 批量营销、隐式入队(文档给"发邮件 = ctx.queue.add"示例)
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* import { email } from 'weifuwu'
|
|
18
|
+
*
|
|
19
|
+
* app.use(email({
|
|
20
|
+
* from: 'no-reply@your.app',
|
|
21
|
+
* adapter: 'resend', // 或 'smtp'
|
|
22
|
+
* resend: { apiKey: process.env.RESEND_API_KEY },
|
|
23
|
+
* }))
|
|
24
|
+
*
|
|
25
|
+
* app.post('/api/notify', async (req, ctx) => {
|
|
26
|
+
* await ctx.email.send({ to: 'user@x.com', subject: '通知', html: '<h1>hi</h1>' })
|
|
27
|
+
* return ok()
|
|
28
|
+
* })
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
import type { Context, Middleware } from '../types.ts';
|
|
32
|
+
import { type SmtpConfig } from './smtp.ts';
|
|
33
|
+
export interface EmailMessage {
|
|
34
|
+
/** 收件人(可多个) */
|
|
35
|
+
to: string | string[];
|
|
36
|
+
subject: string;
|
|
37
|
+
text?: string;
|
|
38
|
+
html?: string;
|
|
39
|
+
/** 覆盖全局 from(可选) */
|
|
40
|
+
from?: string;
|
|
41
|
+
}
|
|
42
|
+
export interface EmailResult {
|
|
43
|
+
/** 服务商返回的邮件 ID(有则给) */
|
|
44
|
+
id?: string;
|
|
45
|
+
accepted: boolean;
|
|
46
|
+
}
|
|
47
|
+
/** 适配器:输入已标准化的邮件消息,输出结果。 */
|
|
48
|
+
export type EmailAdapter = (msg: EmailMessage) => Promise<EmailResult>;
|
|
49
|
+
export interface EmailOptions {
|
|
50
|
+
/** 默认发件人 */
|
|
51
|
+
from: string;
|
|
52
|
+
adapter?: 'resend' | 'smtp' | EmailAdapter;
|
|
53
|
+
resend?: {
|
|
54
|
+
apiKey?: string;
|
|
55
|
+
/** 测试可注入 mock 地址 */
|
|
56
|
+
baseUrl?: string;
|
|
57
|
+
};
|
|
58
|
+
smtp?: SmtpConfig;
|
|
59
|
+
}
|
|
60
|
+
export interface EmailInjected {
|
|
61
|
+
email: {
|
|
62
|
+
send: (msg: EmailMessage) => Promise<EmailResult>;
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
declare module '../types.ts' {
|
|
66
|
+
interface Context {
|
|
67
|
+
email?: EmailInjected['email'];
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
export interface EmailClient extends Middleware<Context, Context & EmailInjected> {
|
|
71
|
+
}
|
|
72
|
+
export declare function email(options: EmailOptions): EmailClient;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 自研 SMTP 客户端(零依赖,node:net + node:tls)
|
|
3
|
+
*
|
|
4
|
+
* 覆盖流程:连接 → 220 → EHLO(特性协商)→ STARTTLS(可选)→ AUTH PLAIN
|
|
5
|
+
* → MAIL FROM → RCPT TO → DATA(dot-stuffing)→ QUIT。
|
|
6
|
+
*
|
|
7
|
+
* 诚实裁剪:
|
|
8
|
+
* - 无连接池/批处理(每次发送新建连接)
|
|
9
|
+
* - 无附件(MIME multipart 未实现,v1 只 text/html)
|
|
10
|
+
* - Subject 非 ASCII 自动 RFC2047 encoded-word(UTF-8)
|
|
11
|
+
* - AUTH 仅 PLAIN(LOGIN/CRAM-MD5 不支持——明确报错而非静默)
|
|
12
|
+
*/
|
|
13
|
+
export interface SmtpConfig {
|
|
14
|
+
host: string;
|
|
15
|
+
/** 默认 465(secure=true)/ 587(secure=false) */
|
|
16
|
+
port?: number;
|
|
17
|
+
user?: string;
|
|
18
|
+
pass?: string;
|
|
19
|
+
/** true = 直连 TLS(465);false = 明文 + STARTTLS(587)。默认 false。 */
|
|
20
|
+
secure?: boolean;
|
|
21
|
+
/** secure=false 时 STARTTLS 失败是否中止。默认 false(继续明文,测试/内网场景)。 */
|
|
22
|
+
requireTls?: boolean;
|
|
23
|
+
timeoutMs?: number;
|
|
24
|
+
/** 自签证书/测试环境放行。默认 false。 */
|
|
25
|
+
rejectUnauthorized?: boolean;
|
|
26
|
+
}
|
|
27
|
+
export interface SmtpMessage {
|
|
28
|
+
from: string;
|
|
29
|
+
to: string[];
|
|
30
|
+
subject: string;
|
|
31
|
+
text?: string;
|
|
32
|
+
html?: string;
|
|
33
|
+
}
|
|
34
|
+
export declare function sendSmtp(cfg: SmtpConfig, msg: SmtpMessage): Promise<void>;
|
package/dist/index.d.ts
CHANGED
|
@@ -11,6 +11,16 @@ export { cors } from './middleware/cors.ts';
|
|
|
11
11
|
export type { CORSOptions } from './middleware/cors.ts';
|
|
12
12
|
export { serveStatic } from './middleware/static.ts';
|
|
13
13
|
export type { ServeStaticOptions } from './middleware/static.ts';
|
|
14
|
+
export { rateLimit } from './middleware/rate-limit.ts';
|
|
15
|
+
export type { RateLimitOptions, RateLimitInjected, RateLimitAlgorithm, RateLimitStore } from './middleware/rate-limit.ts';
|
|
16
|
+
export { email } from './email/index.ts';
|
|
17
|
+
export type { EmailOptions, EmailMessage, EmailResult, EmailAdapter, EmailInjected } from './email/index.ts';
|
|
18
|
+
export { sendSmtp } from './email/smtp.ts';
|
|
19
|
+
export type { SmtpConfig } from './email/smtp.ts';
|
|
20
|
+
export { userSystem } from './user/index.ts';
|
|
21
|
+
export type { UserSystemOptions, UserInjected, AuthApi, RegisterInput } from './user/index.ts';
|
|
22
|
+
export { queue } from './queue/index.ts';
|
|
23
|
+
export type { QueueOptions, QueueClient, QueueInjected, QueueWorker, WorkerOptions, AddOptions, Job } from './queue/index.ts';
|
|
14
24
|
export { ui } from './ui/index.ts';
|
|
15
25
|
export { uiSsr } from './ui/ssr-page.ts';
|
|
16
26
|
export type { UiSsrOptions } from './ui/ssr-page.ts';
|