chanjs 2.7.3 → 2.7.5

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.
Files changed (93) hide show
  1. package/USAGE.md +533 -0
  2. package/config/index.js +37 -6
  3. package/core/App.js +166 -0
  4. package/core/Container.js +77 -0
  5. package/core/Controller.js +29 -0
  6. package/core/Database.js +93 -0
  7. package/core/Repository.js +327 -0
  8. package/core/Service.js +11 -0
  9. package/core/bootstrap/error-handler.js +104 -0
  10. package/core/bootstrap/hook-runner.js +64 -0
  11. package/core/bootstrap/middleware.js +35 -0
  12. package/core/bootstrap/router-loader.js +53 -0
  13. package/core/errors.js +224 -0
  14. package/core/loader.js +89 -0
  15. package/core/registry.js +17 -0
  16. package/doc/Cache.md +279 -106
  17. package/doc/Common.md +590 -134
  18. package/doc/Controller.md +166 -95
  19. package/doc/Help.md +299 -698
  20. package/doc/QuickStart.md +116 -0
  21. package/doc/Repository.md +560 -0
  22. package/doc/Service.md +201 -527
  23. package/index.js +75 -37
  24. package/middleware/body.js +17 -0
  25. package/middleware/cookie.js +7 -15
  26. package/middleware/cors.js +9 -27
  27. package/middleware/favicon.js +7 -17
  28. package/middleware/header.js +15 -16
  29. package/middleware/index.js +11 -11
  30. package/middleware/log.js +26 -56
  31. package/middleware/static.js +15 -28
  32. package/middleware/template.js +75 -115
  33. package/middleware/validate.js +79 -0
  34. package/middleware/waf.js +174 -197
  35. package/package.json +11 -3
  36. package/response/code.js +73 -0
  37. package/response/index.js +9 -6
  38. package/response/response.js +82 -236
  39. package/security/checker.js +26 -74
  40. package/security/index.js +4 -9
  41. package/security/jwt.js +69 -142
  42. package/security/keywords.js +32 -136
  43. package/security/rate-limit.js +38 -80
  44. package/security/sign.js +83 -176
  45. package/security/xss-filter.js +21 -53
  46. package/storage/cache.js +57 -196
  47. package/storage/index.js +3 -6
  48. package/storage/redis.js +123 -181
  49. package/storage/store.js +163 -188
  50. package/utils/data-parse.js +42 -186
  51. package/utils/file.js +73 -244
  52. package/utils/filter.js +22 -25
  53. package/utils/html.js +49 -33
  54. package/utils/index.js +21 -7
  55. package/utils/ip.js +31 -71
  56. package/utils/logger.js +117 -0
  57. package/utils/pages.js +55 -0
  58. package/utils/paths.js +18 -0
  59. package/utils/request.js +94 -136
  60. package/utils/signal.js +87 -0
  61. package/utils/time.js +33 -75
  62. package/utils/tree.js +112 -104
  63. package/App.js +0 -533
  64. package/base/Aop.js +0 -195
  65. package/base/Container.js +0 -161
  66. package/base/Controller.js +0 -65
  67. package/base/Database.js +0 -133
  68. package/base/Event.js +0 -61
  69. package/base/Repository.js +0 -644
  70. package/common/api.js +0 -35
  71. package/common/code.js +0 -52
  72. package/common/email.js +0 -191
  73. package/common/index.js +0 -5
  74. package/common/pages.js +0 -120
  75. package/common/utils.js +0 -73
  76. package/config/code.js +0 -166
  77. package/config/paths.js +0 -60
  78. package/doc/Aop.md +0 -269
  79. package/doc/Email.md +0 -114
  80. package/doc/Event.md +0 -232
  81. package/global/env.js +0 -11
  82. package/global/import.js +0 -39
  83. package/global/index.js +0 -8
  84. package/helper/index.js +0 -79
  85. package/loader/index.js +0 -6
  86. package/loader/loader.js +0 -138
  87. package/middleware/compress.js +0 -185
  88. package/middleware/setBody.js +0 -32
  89. package/realtime/index.js +0 -7
  90. package/realtime/sse.js +0 -424
  91. package/realtime/websocket.js +0 -540
  92. package/schedule/index.js +0 -6
  93. package/schedule/schedule.js +0 -491
package/middleware/waf.js CHANGED
@@ -1,259 +1,236 @@
1
- import crypto from 'crypto';
1
+ import crypto from "crypto";
2
2
  import { getIp } from "../utils/ip.js";
3
3
  import { checkKeywords } from "../security/checker.js";
4
4
  import { filterXSS } from "../security/xss-filter.js";
5
5
  import { createRateLimitMiddleware } from "../security/rate-limit.js";
6
6
  import { store } from "../storage/store.js";
7
+ import { CODE_BLOCKED } from "../response/code.js";
8
+ import logger from "../utils/logger.js";
7
9
 
8
- /**
9
- * Web应用防火墙(WAF)中间件
10
- * 改进点:
11
- * 1. 封禁状态存服务端(Map+TTL 或 Redis),按 IP+UA 维度
12
- * - 彻底废弃 Cookie/localStorage 标记方案(旧版可被任意绕过)
13
- * 2. 命中恶意关键词后封禁一段时间(默认 30 分钟,可配置)
14
- * 3. 支持配置阈值:连续命中 N 次后封禁
15
- * 4. 安全响应头补充 X-Frame-Options / HSTS 等
16
- * 5. 通过 store 适配层:Redis 启用时走 Redis,否则走内存,运行时异常自动降级
17
- */
18
-
19
- // 封禁状态 Redis Key 前缀
20
- const BLOCK_KEY_PREFIX = 'waf:block:';
21
- // 封禁命中次数 Redis Key 前缀
22
- const STRIKE_KEY_PREFIX = 'waf:strike:';
10
+ const BLOCK_KEY_PREFIX = "waf:block:";
11
+ const STRIKE_KEY_PREFIX = "waf:strike:";
23
12
 
24
- /**
25
- * 路径白名单 - 这些路径不会被 WAF 检查
26
- */
13
+ // 放行路径白名单
27
14
  const WAF_PATH_WHITELIST = [
28
- "/.well-known/appspecific/", // Chrome DevTools
29
- "/.well-known/change-password", // 密码管理
30
- "/favicon.ico", // 浏览器自动请求
31
- "/robots.txt", // 搜索引擎爬虫
32
- "/llms.txt", // AI引擎爬虫
33
- "/sitemap.xml", // 站点地图
15
+ "/.well-known/appspecific/",
16
+ "/.well-known/change-password",
17
+ "/favicon.ico",
18
+ "/robots.txt",
19
+ "/llms.txt",
20
+ "/sitemap.xml",
34
21
  ];
35
22
 
36
- /**
37
- * 可信 IP 白名单 - 这些 IP 跳过限流和封禁
38
- * 本机回环 + RFC 1918 三大私有网段,避免开发和内网环境被误限
39
- */
40
- const TRUSTED_IPS = new Set(['127.0.0.1', '::1', '0.0.0.0']);
23
+ const TRUSTED_IPS = new Set(["127.0.0.1", "::1"]);
24
+
25
+ const DEFAULT_BLOCK = {
26
+ BLOCK_DURATION: 30 * 60 * 1000,
27
+ STRIKE_THRESHOLD: 3,
28
+ STRIKE_WINDOW: 60 * 60 * 1000,
29
+ };
41
30
 
42
31
  /**
43
- * 检查 IP 是否可信(本机回环或 RFC 1918 内网)
44
- * RFC 1918 私有网段:
45
- * 10.0.0.0/8 (10.x.x.x)
46
- * 172.16.0.0/12 (172.16.x.x ~ 172.31.x.x)
47
- * 192.168.0.0/16 (192.168.x.x)
32
+ * 判断内网/回环可信IP
48
33
  */
49
- function isTrustedIp(ip) {
34
+ const isTrustedIp = ip => {
50
35
  if (!ip) return false;
51
36
  if (TRUSTED_IPS.has(ip)) return true;
52
- // 10.x.x.x
53
- if (ip.startsWith('10.')) return true;
54
- // 192.168.x.x
55
- if (ip.startsWith('192.168.')) return true;
56
- // 172.16.x.x ~ 172.31.x.x
57
- if (ip.startsWith('172.')) {
58
- const second = parseInt(ip.split('.')[1], 10);
59
- if (second >= 16 && second <= 31) return true;
37
+ if (ip.startsWith("10.") || ip.startsWith("192.168.")) return true;
38
+ if (ip.startsWith("172.")) {
39
+ const seg = Number(ip.split(".")[1]);
40
+ return seg >= 16 && seg <= 31;
60
41
  }
61
42
  return false;
62
- }
43
+ };
63
44
 
64
45
  /**
65
- * 检查路径是否在白名单中
46
+ * 路径白名单匹配
66
47
  */
67
- function isWhitelistedPath(path) {
68
- return WAF_PATH_WHITELIST.some(whitelistPath =>
69
- path.startsWith(whitelistPath) || path === whitelistPath
70
- );
71
- }
48
+ const isWhitelistedPath = path => WAF_PATH_WHITELIST.some(p => path === p || path.startsWith(p));
72
49
 
73
50
  /**
74
- * 生成 IP+UA 维度的标识(sha256 摘要,避免 UA 中特殊字符影响 Key)
75
- * @param {string} ip - 客户端 IP
76
- * @param {string} ua - User-Agent
77
- * @returns {string} 32 位十六进制摘要
51
+ * IP+UA指纹 sha256(前32位)
78
52
  */
79
- function buildFingerprint(ip, ua) {
80
- return crypto
81
- .createHash('sha256')
82
- .update(`${ip}|${ua || ''}`)
83
- .digest('hex')
84
- .slice(0, 32);
85
- }
53
+ const buildFingerprint = (ip, ua = "") =>
54
+ crypto.createHash("sha256").update(`${ip}|${ua}`).digest("hex").slice(0, 32);
86
55
 
87
56
  /**
88
- * 创建 WAF 中间件
89
- * @private
90
- * @param {Object} wafConfig - WAF 配置对象
57
+ * 覆盖Express5只读req.query
91
58
  */
92
- function createWafMiddleware(wafConfig) {
93
- const rateLimitMiddleware = createRateLimitMiddleware(wafConfig.rateLimit);
59
+ const overrideQuery = (req, cleanQuery) => {
60
+ Object.defineProperty(req, "query", {
61
+ value: cleanQuery,
62
+ writable: true,
63
+ enumerable: true,
64
+ configurable: true,
65
+ });
66
+ };
94
67
 
95
- // 封禁配置(默认 30 分钟,命中 3 次恶意关键词后封禁)
96
- const blockConfig = wafConfig.block || {};
97
- const BLOCK_DURATION = blockConfig.duration || 30 * 60 * 1000; // 30 分钟
98
- const STRIKE_THRESHOLD = blockConfig.strikeThreshold || 3; // 命中 3 次封禁
99
- const STRIKE_WINDOW = blockConfig.strikeWindow || 60 * 60 * 1000; // 1 小时内累计
68
+ /**
69
+ * 统一 WAF 拦截响应
70
+ */
71
+ const respondWaf = (res, status, msg, data) =>
72
+ res.status(status).json({ code: CODE_BLOCKED, success: false, msg, data });
73
+
74
+ /**
75
+ * 关键词命中计数+封禁逻辑
76
+ */
77
+ async function handleStrike(fingerprint, clientIp, keyword, blockCfg) {
78
+ const strikeKey = `${STRIKE_KEY_PREFIX}${fingerprint}`;
79
+ const strikes = await store.incrAndExpire(strikeKey, blockCfg.STRIKE_WINDOW);
80
+
81
+ if (strikes >= blockCfg.STRIKE_THRESHOLD) {
82
+ const blockKey = `${BLOCK_KEY_PREFIX}${fingerprint}`;
83
+ await store.set(blockKey, { ip: clientIp, reason: keyword, expireAt: Date.now() + blockCfg.BLOCK_DURATION }, blockCfg.BLOCK_DURATION);
84
+ logger.error(`[WAF封禁] IP:${clientIp} 累计命中:${strikes}次 封禁${blockCfg.BLOCK_DURATION / 60000}分钟`);
85
+ return { blocked: true, strikes };
86
+ }
87
+ return { blocked: false, strikes };
88
+ }
89
+
90
+ /** 执行限流,返回是否已响应 */
91
+ const runRateLimit = (rateLimit, req, res) =>
92
+ new Promise(rateLimit.bind(null, req, res, r)).then(() => res.headersSent);
93
+
94
+ /**
95
+ * 前置WAF中间件(body前)
96
+ */
97
+ const createWafMiddleware = wafConfig => {
98
+ const rateLimit = createRateLimitMiddleware(wafConfig.rateLimit);
99
+ const blockCfg = { ...DEFAULT_BLOCK, ...wafConfig.block };
100
100
 
101
101
  return async (req, res, next) => {
102
102
  try {
103
- if (!wafConfig.enabled) {
103
+ if (!wafConfig.enabled) return next();
104
+
105
+ const clientIp = getIp(req);
106
+ const path = req.path || "";
107
+ const ua = req.headers["user-agent"] || "";
108
+ const fp = buildFingerprint(clientIp, ua);
109
+ const whitePath = isWhitelistedPath(path);
110
+
111
+ // 可信IP直接放行,仅做query XSS过滤
112
+ if (isTrustedIp(clientIp)) {
113
+ if (req.query && Object.keys(req.query).length) overrideQuery(req, filterXSS(req.query));
104
114
  return next();
105
115
  }
106
116
 
107
- const clientIp = getIp(req);
117
+ // 限流
118
+ if (await runRateLimit(rateLimit, req, res)) return;
108
119
 
109
- // 可信 IP(本机/内网)仅跳过限流和封禁状态检查(硬约束:避免误限内网)
110
- // 但仍执行关键词检查和 XSS 过滤,防止 X-Forwarded-For 伪造内网 IP 绕过 WAF(S-09 修复点)
111
- const isTrusted = isTrustedIp(clientIp);
112
-
113
- const requestPath = req.path || "";
114
- const userAgent = req.headers['user-agent'] || '';
115
- const fingerprint = buildFingerprint(clientIp, userAgent);
116
-
117
- // 安全响应头
118
- res.setHeader("X-Content-Type-Options", "nosniff");
119
- res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");
120
- res.setHeader("X-Frame-Options", "DENY");
121
-
122
- // 可信 IP 跳过封禁状态检查(避免误封内网);非可信 IP 才查封禁状态
123
- if (!isTrusted && !isWhitelistedPath(requestPath)) {
124
- const blockKey = `${BLOCK_KEY_PREFIX}${fingerprint}`;
125
- const blocked = await store.get(blockKey);
126
-
127
- if (blocked) {
128
- const remaining = Math.ceil((blocked.expireAt - Date.now()) / 60000);
129
- console.error(`[WAF 封禁拦截] IP:${clientIp} UA:${userAgent.slice(0, 50)} 剩余:${remaining}分钟`);
130
- return res.status(403).json({
131
- code: 403,
132
- success: false,
133
- msg: '您的访问已被限制,请稍后再试',
134
- retryAfter: Math.ceil(BLOCK_DURATION / 1000),
135
- });
136
- }
120
+ // 已封禁直接拦截
121
+ const blockInfo = await store.get(`${BLOCK_KEY_PREFIX}${fp}`);
122
+ if (blockInfo) {
123
+ logger.error(`[WAF拦截封禁] IP:${clientIp} UA:${ua.slice(0, 50)}`);
124
+ return respondWaf(res, 403, "检测到恶意访问,您的访问已被限制", { retryAfter: Math.ceil(blockCfg.BLOCK_DURATION / 1000) });
137
125
  }
138
126
 
139
- // 可信 IP 跳过限流(避免误限内网);非可信 IP 才执行限流
140
- if (!isTrusted) {
141
- await new Promise((resolve) => {
142
- rateLimitMiddleware(req, res, resolve);
143
- });
144
- // 如果限流中间件已经响应(429),不再继续
145
- if (res.headersSent) return;
127
+ // 白名单路径跳过关键词检测
128
+ if (whitePath) {
129
+ if (req.query && Object.keys(req.query).length) overrideQuery(req, filterXSS(req.query));
130
+ return next();
146
131
  }
147
132
 
148
- // 收集待检查文本:路径 + query + body
149
- let checkText = requestPath;
150
- if (req.query && Object.keys(req.query).length > 0) {
151
- const queryStr = Object.entries(req.query)
152
- .map(([k, v]) => `${k}=${v}`)
153
- .join(" ");
133
+ // 路径+query拼接检测恶意关键词
134
+ let checkText = path;
135
+ if (Object.keys(req.query ?? {}).length) {
136
+ const queryStr = Object.entries(req.query).map(([k, v]) => `${k}=${v}`).join(" ");
154
137
  checkText += ` ${queryStr}`;
155
138
  }
139
+ const hit = checkKeywords(checkText);
140
+ if (hit) {
141
+ const { keyword, category } = hit;
142
+ logger.error(`[WAF拦截-URL] IP:${clientIp} Path:${path} Key:${keyword} Type:${category}`);
143
+ const { blocked, strikes } = await handleStrike(fp, clientIp, keyword, blockCfg);
144
+ if (blocked) return respondWaf(res, 403, "检测到恶意访问,您的访问已被限制", { retryAfter: Math.ceil(blockCfg.BLOCK_DURATION / 1000) });
145
+ return respondWaf(res, 403, "检测到非法内容,请求已被拦截", { strikes, threshold: blockCfg.STRIKE_THRESHOLD });
146
+ }
156
147
 
157
- let bodyText = "";
158
- const contentType = req.headers["content-type"]?.toLowerCase() || "";
159
- const isMultipart = contentType.includes("multipart/form-data");
160
-
161
- if (!isMultipart && req.body) {
162
- try {
163
- // 先判断 body 长度再决定是否序列化,避免不必要的 CPU 消耗
164
- const bodyStr = typeof req.body === "string" ? req.body : JSON.stringify(req.body);
165
- if (bodyStr.length < 10000) {
166
- bodyText = ` ${bodyStr}`;
167
- }
168
- } catch (e) {
169
- console.error(`[WAF 警告] body序列化失败 | IP:${clientIp} 错误:${e.message}`);
170
- }
148
+ // query XSS净化
149
+ try {
150
+ if (req.query && Object.keys(req.query).length) overrideQuery(req, filterXSS(req.query));
151
+ } catch (e) {
152
+ logger.error(`[WAF警告] query过滤失败 IP:${clientIp} Err:${e.message}`);
171
153
  }
172
154
 
173
- const fullText = checkText + bodyText;
155
+ next();
156
+ } catch (err) {
157
+ logger.error(`[WAF前置异常] IP:${getIp(req)} Msg:${err.message}`, err.stack);
158
+ return respondWaf(res, 403, "安全检查异常,请求被拒绝");
159
+ }
160
+ };
161
+ };
174
162
 
175
- // 关键词检查(仅对非白名单路径)
176
- if (!isWhitelistedPath(requestPath)) {
177
- const matched = checkKeywords(fullText);
163
+ /**
164
+ * 已登录用户的管理接口路径前缀(跳过 body 关键词检测,避免误杀富文本/代码内容)
165
+ */
166
+ const BODY_CHECK_SKIP_PREFIXES = ["/cms/", "/base/", "/member/", "/book/", "/oss/", "/vip/"];
178
167
 
179
- if (matched) {
180
- const { category, keyword } = matched;
181
- console.error(`[WAF 拦截] IP:${clientIp} 路径:${requestPath} 关键词:${keyword} 类别:${category}`);
168
+ /**
169
+ * Body层WAF中间件(body解析后)
170
+ */
171
+ const createWafBodyMiddleware = wafConfig => {
172
+ const blockCfg = { ...DEFAULT_BLOCK, ...wafConfig.block };
182
173
 
183
- // 记录命中次数,达到阈值则封禁
184
- const strikeKey = `${STRIKE_KEY_PREFIX}${fingerprint}`;
185
- const strikes = await store.incrAndExpire(strikeKey, STRIKE_WINDOW);
174
+ return async (req, res, next) => {
175
+ try {
176
+ if (!wafConfig.enabled) return next();
177
+ const path = req.path || "";
178
+ if (isWhitelistedPath(path)) return next();
186
179
 
187
- if (strikes >= STRIKE_THRESHOLD) {
188
- // 封禁:写入封禁状态,TTL = BLOCK_DURATION
189
- const blockKey = `${BLOCK_KEY_PREFIX}${fingerprint}`;
190
- await store.set(blockKey, { ip: clientIp, reason: keyword, expireAt: Date.now() + BLOCK_DURATION }, BLOCK_DURATION);
191
- console.error(`[WAF 封禁触发] IP:${clientIp} 命中次数:${strikes} 封禁时长:${BLOCK_DURATION / 60000}分钟`);
180
+ const clientIp = getIp(req);
181
+ const contentType = req.headers["content-type"]?.toLowerCase() || "";
182
+ // 上传文件跳过body文本检测
183
+ if (contentType.includes("multipart/form-data")) return next();
184
+ // 空body直接放行
185
+ if (!req.body || (typeof req.body === "object" && !Object.keys(req.body).length)) return next();
192
186
 
193
- return res.status(403).json({
194
- code: 403,
195
- success: false,
196
- msg: '检测到恶意访问,您的访问已被限制',
197
- retryAfter: Math.ceil(BLOCK_DURATION / 1000),
198
- });
199
- }
187
+ // 已登录用户的管理接口:跳过关键词检测(富文本/代码内容会误杀),仅做 XSS 过滤
188
+ const isAuthedAdmin = req.user?.uid && BODY_CHECK_SKIP_PREFIXES.some(p => path.startsWith(p));
200
189
 
201
- // 未达阈值,仅拦截本次请求
202
- return res.status(403).json({
203
- code: 403,
204
- success: false,
205
- msg: '检测到非法内容,请求已被拦截',
206
- data: { strikes, threshold: STRIKE_THRESHOLD },
207
- });
190
+ if (!isAuthedAdmin) {
191
+ // 序列化body文本,截断1w字符防绕过
192
+ let bodyText = "";
193
+ try {
194
+ bodyText = (typeof req.body === "string" ? req.body : JSON.stringify(req.body)).slice(0, 10000);
195
+ } catch (e) {
196
+ logger.error(`[WAF警告] body序列化失败 IP:${clientIp} Err:${e.message}`);
208
197
  }
209
- }
210
198
 
211
- // XSS 过滤
212
- try {
213
- if (req.query && typeof req.query === 'object') {
214
- const filteredQuery = filterXSS(req.query);
215
- Object.keys(req.query).forEach(key => delete req.query[key]);
216
- Object.assign(req.query, filteredQuery);
199
+ // body关键词检测拦截
200
+ if (bodyText) {
201
+ const hit = checkKeywords(bodyText);
202
+ if (hit) {
203
+ const { keyword, category } = hit;
204
+ const fp = buildFingerprint(clientIp, req.headers["user-agent"] || "");
205
+ logger.error(`[WAF拦截-Body] IP:${clientIp} Path:${path} Key:${keyword} Type:${category}`);
206
+ const { blocked, strikes } = await handleStrike(fp, clientIp, keyword, blockCfg);
207
+ if (blocked) return respondWaf(res, 403, "检测到恶意访问,您的访问已被限制", { retryAfter: Math.ceil(blockCfg.BLOCK_DURATION / 1000) });
208
+ return respondWaf(res, 403, "检测到非法内容,请求已被拦截", { strikes, threshold: blockCfg.STRIKE_THRESHOLD });
209
+ }
217
210
  }
211
+ }
218
212
 
219
- if (req.body && typeof req.body === 'object') {
220
- req.body = filterXSS(req.body);
221
- }
213
+ // body XSS过滤
214
+ try {
215
+ if (typeof req.body === "object") req.body = filterXSS(req.body);
222
216
  } catch (e) {
223
- console.error(`[WAF 警告] XSS过滤失败 | IP:${clientIp} 错误:${e.message}`);
217
+ logger.error(`[WAF警告] body过滤失败 IP:${clientIp} Err:${e.message}`);
224
218
  }
225
219
 
226
220
  next();
227
- } catch (error) {
228
- console.error(`[WAF 异常] IP:${getIp(req)} 错误:${error.message}`, error.stack);
229
- // 异常时拒绝请求,避免 WAF 故障时被绕过(fail-closed)
230
- return res.status(403).json({
231
- code: 403,
232
- success: false,
233
- msg: '安全检查异常,请求被拒绝',
234
- });
221
+ } catch (err) {
222
+ logger.error(`[WAF Body异常] IP:${getIp(req)} Msg:${err.message}`);
223
+ return respondWaf(res, 403, "安全检查异常,请求被拒绝");
235
224
  }
236
225
  };
237
- }
226
+ };
238
227
 
239
228
  /**
240
- * 设置 WAF 中间件
241
- * @param {Object} app - Express 应用实例
242
- * @param {Object} config - WAF 配置对象
243
- * @param {boolean} config.enabled - 是否启用 WAF
244
- * @param {Object} config.rateLimit - 限流配置
245
- * @param {Object} [config.block] - 封禁配置
246
- * @param {number} [config.block.duration=1800000] - 封禁时长(毫秒),默认 30 分钟
247
- * @param {number} [config.block.strikeThreshold=3] - 命中恶意关键词几次后封禁
248
- * @param {number} [config.block.strikeWindow=3600000] - 命中累计窗口(毫秒)
249
- * @example
250
- * waf(app, {
251
- * enabled: true,
252
- * rateLimit: { windowMs: 60000, max: 100 },
253
- * block: { duration: 1800000, strikeThreshold: 3, strikeWindow: 3600000 }
254
- * });
229
+ * 注册WAF前置中间件(body前)
255
230
  */
256
- export const waf = function (app, config) {
257
- const middleware = createWafMiddleware(config);
258
- app.use(middleware);
259
- };
231
+ export const waf = (app, config) => app.use(createWafMiddleware(config));
232
+
233
+ /**
234
+ * 注册WAF Body中间件(body后)
235
+ */
236
+ export const wafBody = (app, config) => app.use(createWafBodyMiddleware(config));
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "type": "module",
3
3
  "name": "chanjs",
4
- "version": "2.7.3",
4
+ "version": "2.7.5",
5
5
  "description": "chanjs基于express5 纯js研发的轻量级mvc框架。",
6
6
  "main": "index.js",
7
7
  "module": "index.js",
@@ -32,7 +32,15 @@
32
32
  "morgan": "^1.10.1",
33
33
  "mysql2": "^3.22.3",
34
34
  "serve-favicon": "^2.5.1",
35
- "ws": "^8.18.0",
36
- "xss": "^1.0.15"
35
+ "xss": "^1.0.15",
36
+ "ioredis": "^5.4.6"
37
+ },
38
+ "peerDependencies": {
39
+ "zod": "^3.0.0"
40
+ },
41
+ "peerDependenciesMeta": {
42
+ "zod": {
43
+ "optional": true
44
+ }
37
45
  }
38
46
  }
@@ -0,0 +1,73 @@
1
+ /**
2
+ * 业务状态码统一规范:与HTTP状态码解耦,0为成功,分段划分错误域
3
+ */
4
+
5
+ // 业务码-默认提示文案映射
6
+ export const CODE = Object.freeze({
7
+ // 成功
8
+ 0: "操作成功",
9
+ // 1xxx 通用业务错误
10
+ 1001: "认证失败",
11
+ 1002: "令牌已过期",
12
+ 1003: "权限不足",
13
+ 1004: "资源不存在",
14
+ 1005: "资源已存在",
15
+ 1006: "参数无效",
16
+ 1007: "参数缺失",
17
+ 1008: "业务处理失败",
18
+ 1009: "请求过于频繁",
19
+ 1010: "登录设备异常",
20
+ 1011: "访问已被限制",
21
+ // 5xxx 系统错误
22
+ 5001: "系统内部错误",
23
+ 5002: "服务繁忙,请稍后再试",
24
+ // 6xxx 数据库错误
25
+ 6001: "数据库连接失败",
26
+ 6002: "数据库访问被拒绝",
27
+ 6003: "存在关联数据,操作失败",
28
+ 6004: "数据库字段错误",
29
+ 6005: "数据重复,违反唯一性约束",
30
+ 6006: "目标表不存在",
31
+ 6007: "数据库操作超时",
32
+ 6008: "数据库语法错误,请检查查询语句",
33
+ 6009: "数据库连接已关闭,请重试",
34
+ });
35
+
36
+ // 语义化常量(框架内直接引用,避免硬编码数字)
37
+ export const CODE_OK = 0;
38
+ export const CODE_AUTH_FAILED = 1001;
39
+ export const CODE_TOKEN_EXPIRED = 1002;
40
+ export const CODE_FORBIDDEN = 1003;
41
+ export const CODE_NOT_FOUND = 1004;
42
+ export const CODE_CONFLICT = 1005;
43
+ export const CODE_PARAM_INVALID = 1006;
44
+ export const CODE_PARAM_MISSING = 1007;
45
+ export const CODE_BUSINESS_FAIL = 1008;
46
+ export const CODE_RATE_LIMIT = 1009;
47
+ export const CODE_DEVICE_ERROR = 1010;
48
+ export const CODE_BLOCKED = 1011;
49
+ export const CODE_SYSTEM_ERROR = 5001;
50
+ export const CODE_SERVICE_BUSY = 5002;
51
+ export const CODE_DB_CONNECTION_ERROR = 6001;
52
+ export const CODE_DB_ACCESS_DENIED = 6002;
53
+ export const CODE_DB_OPERATION_TIMEOUT = 6007;
54
+
55
+ // 数据库原生错误标识 → 业务码映射
56
+ export const DB_ERROR = Object.freeze({
57
+ ECONNREFUSED: 6001,
58
+ ER_ACCESS_DENIED_ERROR: 6002,
59
+ ER_ROW_IS_REFERENCED_2: 6003,
60
+ ER_BAD_FIELD_ERROR: 6004,
61
+ ER_DUP_ENTRY: 6005,
62
+ ER_NO_SUCH_TABLE: 6006,
63
+ ETIMEOUT: 6007,
64
+ ER_TABLE_EXISTS_ERROR: 1005,
65
+ });
66
+
67
+ /**
68
+ * 根据业务码获取默认提示文案
69
+ * @param {number} code 业务状态码
70
+ * @param {string} [fallback="操作失败"] 兜底文案
71
+ * @returns {string}
72
+ */
73
+ export const getCodeMsg = (code, fallback = "操作失败") => CODE[code] ?? fallback;
package/response/index.js CHANGED
@@ -1,12 +1,15 @@
1
1
  /**
2
- * 响应格式化模块 - 统一响应结构 + 数据库错误解析
3
- * 响应码体系:0 成功 / 1xxx 业务 / 5xxx 系统 / 6xxx 数据库
2
+ * response 模块聚合导出
3
+ * 对外暴露:success / fail / routeNotFound / serializeError
4
4
  */
5
5
  export {
6
6
  success,
7
7
  fail,
8
- error,
9
- parseDatabaseError,
10
- notFoundResponse,
11
- errorResponse,
8
+ routeNotFound,
9
+ serializeError,
10
+ buildErrorHtml,
11
+ respondError,
12
12
  } from "./response.js";
13
+
14
+ // CODE 字典对外暴露(业务侧 common.CODE 需要)
15
+ export { CODE } from "./code.js";