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/index.js CHANGED
@@ -1,37 +1,75 @@
1
- // 核心基础类导出
2
- export { default as Controller } from "./base/Controller.js";
3
- export { default as Repository } from "./base/Repository.js";
4
- export { default as DB } from "./base/Database.js";
5
- export { Container } from "./base/Container.js";
6
- export { Aop, aop } from "./base/Aop.js";
7
- export { Event, event } from "./base/Event.js";
8
-
9
- // 工具模块导出(按职责拆分,替代原 helper 大杂烩)
10
- export * as storage from "./storage/index.js"; // 缓存/Redis/存储适配
11
- export * as security from "./security/index.js"; // 关键词/XSS/签名/JWT/限流
12
- export * as realtime from "./realtime/index.js"; // SSE/WebSocket
13
- export * as loader from "./loader/index.js"; // 加载器
14
- export * as response from "./response/index.js"; // 响应格式化
15
- export * as utils from "./utils/index.js"; // 通用工具(时间/文件/HTML/IP/数据解析/树/过滤)
16
- export * as helper from "./helper/index.js"; // 聚合导出层(从各子模块 re-export,业务侧兼容)
17
- export * as middleware from "./middleware/index.js";
18
- export * as config from "./config/index.js";
19
- export * as common from "./common/index.js";
20
-
21
- // 常用工具函数直接导出(顶层快捷访问)
22
- export { loadConfig, loaderSort, loadController } from "./loader/index.js";
23
- export { cache } from "./storage/index.js";
24
-
25
- // 定时任务调度器
26
- export { schedule, Schedule } from "./schedule/schedule.js";
27
-
28
- // 实时通信:SSE + WebSocket
29
- export { sse, SSEManager } from "./realtime/index.js";
30
- export { websocket, WebSocketManager } from "./realtime/index.js";
31
-
32
- // 路径配置
33
- export { Paths } from "./config/paths.js";
34
-
35
- // 默认导出应用主类
36
- import Chan from "./App.js";
37
- export default Chan;
1
+ /**
2
+ * chanjs 包入口
3
+ *
4
+ * 默认导出:Chan 应用主类
5
+ * import Chan from "chanjs";
6
+ * const chan = new Chan();
7
+ * await chan.start();
8
+ *
9
+ * 命名导出:
10
+ * import { Controller, Repository, Service, helper, cache, Paths, loader, utils, getApp } from "chanjs";
11
+ * import { validate, validateAll } from "chanjs"; // zod 校验中间件
12
+ * import { AppError, NotFoundError, ValidationError } from "chanjs"; // 错误类
13
+ *
14
+ * 多实例:
15
+ * 每个 Chan 实例持有独立 config/db/paths/dbManager(注册表管理),
16
+ * 业务代码统一通过 getApp() this.app(Controller/Service 内)访问当前实例。
17
+ */
18
+
19
+ // ===================== 核心基础类 =====================
20
+ export { default as Controller } from "./core/Controller.js";
21
+ export { default as Repository } from "./core/Repository.js";
22
+ export { Service } from "./core/Service.js";
23
+
24
+ // ===================== 错误体系 =====================
25
+ export {
26
+ AppError, AuthError, TokenExpiredError, ForbiddenError,
27
+ NotFoundError, ConflictError, ValidationError, ParamMissingError,
28
+ BusinessError, RateLimitError, BlockedError, SystemError, ServiceBusyError,
29
+ describeError, errorExtraProps, parseStack, isAppError, wrapDbError,
30
+ } from "./core/errors.js";
31
+
32
+ // ===================== 统一响应工具(纯函数,可复用于接口 / 定时任务 / RPC)=====================
33
+ export { success, fail, routeNotFound, serializeError, buildErrorHtml, respondError } from "./response/index.js";
34
+
35
+ // ===================== 安全工具 =====================
36
+ export { setToken, getToken, verifyToken } from "./security/jwt.js";
37
+ export { aesEncrypt, aesDecrypt } from "./security/sign.js";
38
+ export { createRateLimitMiddleware } from "./security/rate-limit.js";
39
+ export { filterXSS, checkKeywords } from "./security/index.js";
40
+
41
+ // ===================== 统一日志 =====================
42
+ export { default as logger, createLogger } from "./utils/logger.js";
43
+
44
+ // ===================== 校验中间件 =====================
45
+ export { validate, validateAll } from "./middleware/validate.js";
46
+
47
+ // ===================== 应用实例(单实例)=====================
48
+ export { setApp, getApp } from "./core/registry.js";
49
+
50
+ // ===================== 命名空间导出(业务侧 router/service 使用)=====================
51
+ export * as loader from "./core/loader.js";
52
+ export * as utils from "./utils/index.js";
53
+
54
+ // ===================== 常用工具直接导出 =====================
55
+ export { cache, store } from "./storage/index.js";
56
+ export { Paths } from "./utils/paths.js";
57
+
58
+ // ===================== helper 聚合对象 =====================
59
+ // 为兼容老代码 import { helper } from "chanjs",薄封装:单一来源 = 命名导出 + 工具命名空间
60
+ import * as utilsNs from "./utils/index.js";
61
+ import { cache, store } from "./storage/index.js";
62
+ import { success, fail } from "./response/index.js";
63
+ import { setToken, getToken, aesEncrypt, aesDecrypt } from "./security/index.js";
64
+ import { createRateLimitMiddleware } from "./security/rate-limit.js";
65
+ import { filterXSS, checkKeywords } from "./security/index.js";
66
+
67
+ export const helper = Object.assign(Object.create(null), utilsNs, {
68
+ cache, store, success, fail,
69
+ setToken, getToken, aesEncrypt, aesDecrypt,
70
+ createRateLimitMiddleware, filterXSS, checkKeywords,
71
+ });
72
+
73
+ // ===================== 默认导出应用主类 =====================
74
+ import Chan from "./core/App.js";
75
+ export default Chan;
@@ -0,0 +1,17 @@
1
+ import express from "express";
2
+
3
+ /**
4
+ * @description 请求体解析中间件,支持 JSON/URL-XML
5
+ * 安全限制:请求体上限、参数数量防攻击,支持嵌套对象
6
+ * @param {express.Application} app Express实例
7
+ * @param {string} [bodyLimit='10mb'] 请求体大小限制
8
+ */
9
+ export function body(app, bodyLimit = '10mb') {
10
+ app.use(express.raw({ type: "application/xml", limit: bodyLimit }));
11
+ app.use(express.json({ limit: bodyLimit }));
12
+ app.use(express.urlencoded({
13
+ extended: true,
14
+ limit: bodyLimit,
15
+ parameterLimit: 100,
16
+ }));
17
+ }
@@ -1,20 +1,12 @@
1
1
  import cookieParser from "cookie-parser";
2
+ import logger from "../utils/logger.js";
2
3
 
3
4
  /**
4
- * Cookie 中间件配置
5
- * 配置 Express 应用的 cookie 解析功能
5
+ * Cookie解析中间件,支持签名校验
6
+ * @param {express.Application} app Express实例
7
+ * @param {string} [cookieKey] Cookie签名密钥
6
8
  */
7
-
8
- /**
9
- * 设置 Cookie 解析中间件
10
- * @param {Object} app - Express 应用实例
11
- * @param {string} cookieKey - Cookie 签名密钥
12
- * @description
13
- * 为 Express 应用配置 cookie-parser 中间件
14
- * 使用密钥对 cookie 进行签名验证
15
- * @example
16
- * setCookie(app, 'my-secret-key');
17
- */
18
- export const setCookie = (app, cookieKey) => {
9
+ export const cookie = (app, cookieKey) => {
10
+ !cookieKey && logger.warn("[Cookie] 未配置签名密钥,Cookie无法加密签名");
19
11
  app.use(cookieParser(cookieKey));
20
- };
12
+ };
@@ -1,36 +1,18 @@
1
1
  import cors from "cors";
2
2
 
3
3
  /**
4
- * CORS 中间件配置
5
- * 配置跨域资源共享
6
- */
7
-
8
- /**
9
- * 设置 CORS 中间件
10
- * @param {Object} app - Express 应用实例
11
- * @param {Object} [_cors={}] - CORS 配置选项
12
- * @description
13
- * 为 Express 应用配置 CORS 中间件
14
- *
15
- * 安全改进:
16
- * - 默认拒绝跨域(origin=false),避免未配置时全开放
17
- * - 默认仅允许 GET/POST 方法
18
- * - credentials 默认 false,避免 Cookie 跨域携带
19
- * - 业务侧需显式开放时再传入完整 _cors 配置
20
- * @example
21
- * Cors(app, {
22
- * origin: 'https://example.com',
23
- * methods: ['GET', 'POST', 'PUT', 'DELETE'],
24
- * credentials: true
25
- * });
4
+ * CORS跨域中间件,内置安全默认值
5
+ * @param {express.Application} app Express实例
6
+ * @param {cors.CorsOptions} [_cors={}] 自定义跨域配置
7
+ * 默认安全策略:禁止跨域、仅GET/POST、不携带Cookie
26
8
  */
27
9
  export const Cors = (app, _cors = {}) => {
28
10
  const safeOptions = {
29
- origin: _cors.origin || false,
30
- methods: _cors.methods || ['GET', 'POST'],
31
- allowedHeaders: _cors.allowedHeaders || ['Content-Type', 'Authorization'],
11
+ origin: _cors.origin ?? false,
12
+ methods: _cors.methods ?? ["GET", "POST"],
13
+ allowedHeaders: _cors.allowedHeaders ?? ["Content-Type", "Authorization"],
32
14
  credentials: !!_cors.credentials,
33
- maxAge: _cors.maxAge || 600,
15
+ maxAge: _cors.maxAge ?? 600,
34
16
  };
35
17
  app.use(cors(safeOptions));
36
- };
18
+ };
@@ -1,21 +1,11 @@
1
1
  import path from "path";
2
- import favicon from "serve-favicon";
3
- import { Paths } from "../config/paths.js";
2
+ import serveFavicon from "serve-favicon";
3
+ import { Paths } from "../utils/paths.js";
4
4
 
5
5
  /**
6
- * Favicon 中间件配置
7
- * 配置网站图标
6
+ * 网站图标中间件,读取public目录favicon.ico
7
+ * @param {express.Application} app Express实例
8
8
  */
9
-
10
- /**
11
- * 设置 Favicon 中间件
12
- * @param {Object} app - Express 应用实例
13
- * @description
14
- * 为 Express 应用配置 favicon 服务
15
- * 从 public 目录加载 favicon.ico 文件
16
- * @example
17
- * setFavicon(app);
18
- */
19
- export const setFavicon = (app) => {
20
- app.use(favicon(path.join(Paths.publicPath, "favicon.ico")));
21
- };
9
+ export const favicon = app => {
10
+ app.use(serveFavicon(path.join(Paths.publicPath, "favicon.ico")));
11
+ };
@@ -1,21 +1,20 @@
1
1
  /**
2
- * 响应头中间件配置
3
- * 设置应用相关的响应头信息
2
+ * 自定义响应头中间件,隐藏原生Express标识,统一输出ChanCMS
3
+ * @param {express.Application} app Express实例
4
+ * @param {{APP_NAME?: string, APP_VERSION?: string}} [opts] 配置选项
4
5
  */
6
+ export const header = (app, opts = {}) => {
7
+ const appName = opts.APP_NAME || "ChanCMS";
8
+ const appVer = opts.APP_VERSION || "";
9
+ const poweredBy = appVer ? `${appName}/${appVer}` : appName;
5
10
 
6
- /**
7
- * 设置响应头中间件
8
- * @param {Object} app - Express 应用实例
9
- * @param {Object} options - 配置选项(保留参数兼容旧调用,不再使用 APP_VERSION)
10
- * @description
11
- * 为所有响应添加技术栈标识响应头
12
- * 保留 ChanCMS 标识,移除版本号防止信息泄露
13
- * @example
14
- * setHeader(app, { APP_NAME: 'MyApp' });
15
- */
16
- export let setHeader = (app) => {
17
- app.use((req, res, next) => {
18
- res.setHeader("X-Powered-By", "ChanCMS");
11
+ app.use((_, res, next) => {
12
+ res.setHeader("X-Powered-By", poweredBy);
13
+ // 常驻安全响应头:无论 WAF 是否启用都生效
14
+ // 禁止 MIME 嗅探(防 XSS 利用 Content-Type 歧义)
15
+ res.setHeader("X-Content-Type-Options", "nosniff");
16
+ // 防点击劫持:仅允许同源 iframe 嵌套(如需被第三方嵌入请按需放宽)
17
+ res.setHeader("Content-Security-Policy", "frame-ancestors 'self'");
19
18
  next();
20
19
  });
21
- };
20
+ };
@@ -1,15 +1,15 @@
1
1
  /**
2
- * 中间件模块入口
3
- * 导出所有中间件配置函数
2
+ * middleware 模块聚合导出
3
+ *
4
+ * 说明:响应压缩(compress)已从框架移除——生产环境由 Nginx gzip 统一处理,
5
+ * 无需在 Node 层重复实现。
4
6
  */
5
-
6
- export { setCookie } from "./cookie.js";
7
- export { setFavicon } from "./favicon.js";
8
- export { setBody } from "./setBody.js";
9
- export { setStatic } from "./static.js";
10
- export { setHeader } from "./header.js";
11
- export { setTemplate } from "./template.js";
12
7
  export { Cors } from "./cors.js";
13
- export { waf } from "./waf.js";
14
- export { compress } from "./compress.js";
8
+ export { body } from "./body.js";
9
+ export { cookie } from "./cookie.js";
10
+ export { favicon } from "./favicon.js";
11
+ export { header } from "./header.js";
12
+ export { staticMw } from "./static.js";
13
+ export { template } from "./template.js";
14
+ export { waf, wafBody } from "./waf.js";
15
15
  export { log } from "./log.js";
package/middleware/log.js CHANGED
@@ -1,65 +1,35 @@
1
1
  import morgan from "morgan";
2
2
  import { getIp } from "../utils/ip.js";
3
3
 
4
- /**
5
- * morgan 输出格式白名单
6
- * 仅允许使用以下格式,避免任意 level 注入导致 morgan 内部异常
7
- */
8
- const ALLOWED_FORMATS = new Set([
9
- 'chancms',
10
- 'combined',
11
- 'common',
12
- 'dev',
13
- 'short',
14
- 'tiny',
15
- ]);
16
-
17
- // 自定义 IP 令牌
18
- morgan.token("ip", (req, res) => {
19
- return getIp(req);
20
- });
21
-
22
- // 自定义用户信息令牌
23
- morgan.token("user", (req, res) => {
24
- if (req.user) {
25
- return `${req.user.uid}:${req.user.username}`;
26
- }
27
- return "-";
28
- });
4
+ // 日志格式白名单,防止非法格式注入
5
+ const ALLOWED_FORMATS = new Set(["chancms", "combined", "common", "dev", "short", "tiny"]);
29
6
 
30
- // 自定义时间令牌(带日期)
31
- morgan.token("datetime", (req, res) => {
32
- return new Date().toISOString();
33
- });
7
+ // 自定义token
8
+ morgan.token("ip", req => getIp(req));
9
+ morgan.token("user", req => req.user ? `${req.user.uid}:${req.user.username}` : "-");
10
+ morgan.token("datetime", () => new Date().toISOString());
34
11
 
35
- // 自定义 morgan 格式:时间 IP 用户 Method URL Status Length - Response-Time ms
36
- morgan.format("chancms", (tokens, req, res) => {
37
- return [
38
- tokens.datetime(req, res),
39
- tokens.ip(req, res),
40
- tokens.user(req, res),
41
- tokens.method(req, res),
42
- tokens.url(req, res),
43
- tokens.status(req, res),
44
- tokens.res(req, res, "content-length") || "-",
45
- "-",
46
- tokens["response-time"](req, res),
47
- "ms",
48
- ].join(" ");
49
- });
12
+ // 自定义ChanCMS日志输出模板
13
+ morgan.format("chancms", (tokens, req, res) => [
14
+ tokens.datetime(req, res),
15
+ tokens.ip(req, res),
16
+ tokens.user(req, res),
17
+ tokens.method(req, res),
18
+ tokens.url(req, res),
19
+ tokens.status(req, res),
20
+ tokens.res(req, res, "content-length") || "-",
21
+ "-",
22
+ tokens["response-time"](req, res),
23
+ "ms"
24
+ ].join(" "));
50
25
 
51
26
  /**
52
- * 注册 morgan 日志中间件
53
- * @param {Object} app - Express 应用实例
54
- * @param {Object} [logger] - 日志配置
55
- * @param {string} [logger.level='chancms'] - 日志格式,仅允许白名单内的格式
56
- * @description
57
- * 安全改进:
58
- * - level 必须在白名单内,未配置或非法值默认 'chancms'
59
- * - 避免任意字符串传入 morgan 导致格式解析异常
27
+ * 请求日志中间件(morgan),内置格式白名单防注入
28
+ * @param {express.Application} app Express实例
29
+ * @param {{level?: string}} [logger={}] 日志配置
60
30
  */
61
- export const log = (app, logger) => {
62
- const level = logger?.level;
63
- const format = ALLOWED_FORMATS.has(level) ? level : 'chancms';
31
+ export const log = (app, logger = {}) => {
32
+ const level = logger.level;
33
+ const format = ALLOWED_FORMATS.has(level) ? level : "chancms";
64
34
  app.use(morgan(format));
65
- };
35
+ };
@@ -1,32 +1,19 @@
1
1
  import express from "express";
2
+ import { safePath } from "../utils/file.js";
3
+ import logger from "../utils/logger.js";
2
4
 
3
5
  /**
4
- * 静态文件服务中间件配置
5
- * 配置静态资源访问
6
+ * 批量挂载静态资源中间件,内置目录安全校验
7
+ * @param {express.Application} app Express实例
8
+ * @param {Array<{prefix:string;dir:string;maxAge?:number}>} statics 静态目录配置数组
6
9
  */
7
-
8
- /**
9
- * 设置静态文件服务中间件
10
- * @param {Object} app - Express 应用实例
11
- * @param {Array<Object>} statics - 静态资源配置数组
12
- * @param {string} statics[].prefix - URL 前缀
13
- * @param {string} statics[].dir - 静态文件目录
14
- * @param {number} [statics[].maxAge] - 缓存时间(秒)
15
- * @description
16
- * 为 Express 应用配置多个静态文件服务
17
- * 每个静态资源可以指定不同的 URL 前缀和目录
18
- * @example
19
- * setStatic(app, [
20
- * { prefix: '/public', dir: './public', maxAge: 3600 },
21
- * { prefix: '/uploads', dir: './uploads', maxAge: 0 }
22
- * ]);
23
- */
24
- export const setStatic = async function (app, statics) {
25
- if (statics.length > 0) {
26
- statics.forEach((item) => {
27
- const { prefix, dir, maxAge } = item;
28
- // dotfile: 'deny' 显式拒绝访问点文件(.env / .git/config 等),避免敏感配置泄漏
29
- app.use(prefix, express.static(dir, { maxAge: maxAge || 0, dotfile: 'deny' }));
30
- });
31
- }
32
- };
10
+ export const staticMw = async (app, statics) => {
11
+ if (!Array.isArray(statics) || !statics.length) return;
12
+ statics.forEach(({ prefix, dir, maxAge }) => {
13
+ if (!safePath(dir)) {
14
+ logger.error(`[Static] 不安全目录拦截:${dir}`);
15
+ return;
16
+ }
17
+ app.use(prefix, express.static(dir, { maxAge: maxAge ?? 0, dotfile: "deny" }));
18
+ });
19
+ };