befly 3.76.0 → 3.76.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.
Files changed (155) hide show
  1. package/Befly.js +159 -0
  2. package/README.md +50 -11
  3. package/apis/admin/delete.js +1 -3
  4. package/apis/admin/insert.js +2 -1
  5. package/apis/admin/select.js +2 -7
  6. package/apis/admin/update.js +3 -8
  7. package/apis/api/select.js +2 -7
  8. package/apis/auth/login.js +16 -20
  9. package/apis/dict/delete.js +1 -1
  10. package/apis/dict/detail.js +1 -3
  11. package/apis/dict/select.js +1 -2
  12. package/apis/dict/update.js +1 -1
  13. package/apis/dictType/delete.js +1 -1
  14. package/apis/dictType/detail.js +1 -1
  15. package/apis/dictType/insert.js +1 -1
  16. package/apis/dictType/select.js +2 -7
  17. package/apis/dictType/update.js +2 -2
  18. package/apis/email/config.js +4 -7
  19. package/apis/email/logList.js +2 -7
  20. package/apis/email/send.js +1 -2
  21. package/apis/email/verify.js +1 -3
  22. package/apis/loginLog/select.js +2 -7
  23. package/apis/menu/select.js +2 -7
  24. package/apis/operateLog/select.js +2 -7
  25. package/apis/role/apiSave.js +2 -2
  26. package/apis/role/menuSave.js +2 -2
  27. package/apis/role/select.js +2 -7
  28. package/apis/source/imageList.js +2 -5
  29. package/apis/tongJi/dailyReport.js +12 -13
  30. package/apis/tongJi/dailyReportSelect.js +1 -2
  31. package/apis/tongJi/dailyStats.js +1 -1
  32. package/apis/tongJi/dailyStatsDistribution.js +2 -2
  33. package/apis/tongJi/errorList.js +1 -2
  34. package/apis/tongJi/errorReport.js +1 -2
  35. package/apis/tongJi/projectDelete.js +1 -3
  36. package/apis/tongJi/projectSelect.js +2 -4
  37. package/apis/tongJi/projectUpdate.js +2 -2
  38. package/apis/upload/file.js +7 -5
  39. package/checks/api.js +43 -66
  40. package/checks/config.js +17 -12
  41. package/checks/corns.js +28 -46
  42. package/checks/field.js +18 -0
  43. package/checks/items.js +16 -60
  44. package/checks/menu.js +28 -39
  45. package/checks/table.js +106 -26
  46. package/checks/validation.js +13 -0
  47. package/configs/beflyConfig.json +6 -6
  48. package/configs/constConfig.js +2 -63
  49. package/exports.js +2 -0
  50. package/hooks/auth.js +11 -7
  51. package/hooks/parser.js +55 -59
  52. package/hooks/permission.js +5 -2
  53. package/hooks/rateLimit.js +25 -62
  54. package/hooks/validator.js +43 -31
  55. package/index.js +43 -344
  56. package/{lib → libs}/cacheHelper.js +3 -8
  57. package/libs/cron.js +60 -0
  58. package/libs/logger/index.js +1 -0
  59. package/libs/logger/logger.js +245 -0
  60. package/libs/logger/sanitize.js +115 -0
  61. package/{lib → libs/mysql}/dbHelper.js +100 -73
  62. package/{lib → libs/mysql}/dbParse.js +16 -9
  63. package/{lib → libs/mysql}/dbUtil.js +15 -32
  64. package/libs/mysql/error.js +29 -0
  65. package/libs/mysql/index.js +6 -0
  66. package/libs/mysql/is.js +29 -0
  67. package/libs/mysql/mysql.js +43 -0
  68. package/libs/mysql/sql/identifier.js +85 -0
  69. package/libs/mysql/sql/index.js +2 -0
  70. package/{lib → libs/mysql/sql}/sqlBuilder.js +2 -3
  71. package/libs/mysql/util.js +44 -0
  72. package/libs/redis/index.js +2 -0
  73. package/libs/redis/redis.js +204 -0
  74. package/libs/redis/redisError.js +8 -0
  75. package/libs/tableModel.js +72 -0
  76. package/libs/tool.js +33 -0
  77. package/libs/validator/compiler.js +248 -0
  78. package/libs/validator/index.js +10 -0
  79. package/libs/validator/parser.js +173 -0
  80. package/libs/validator/util.js +5 -0
  81. package/package.json +10 -8
  82. package/paths.js +2 -3
  83. package/plugins/cache.js +2 -1
  84. package/plugins/email.js +4 -4
  85. package/plugins/mysql.js +5 -15
  86. package/plugins/redis.js +4 -12
  87. package/router/api.js +14 -10
  88. package/router/root.js +14 -0
  89. package/router/static.js +3 -3
  90. package/schemas/api.json +99 -0
  91. package/schemas/config.json +255 -0
  92. package/schemas/corn.json +39 -0
  93. package/schemas/item.json +43 -0
  94. package/schemas/menu.json +44 -0
  95. package/schemas/table.json +80 -0
  96. package/scripts/syncDb/apply.js +87 -0
  97. package/scripts/syncDb/ddl.js +43 -0
  98. package/scripts/syncDb/index.js +175 -30
  99. package/scripts/syncDb/model.js +60 -0
  100. package/scripts/syncDb/plan.js +160 -0
  101. package/scripts/syncDb/preflight.js +98 -0
  102. package/scripts/syncDb/report.js +177 -162
  103. package/scripts/syncDb/snapshot.js +118 -0
  104. package/sync/syncUtil.js +7 -13
  105. package/tables/admin.json +51 -53
  106. package/tables/api.json +29 -40
  107. package/tables/dailyReport.json +85 -73
  108. package/tables/dict.json +27 -39
  109. package/tables/dictType.json +20 -34
  110. package/tables/emailLog.json +49 -47
  111. package/tables/errorReport.json +86 -60
  112. package/tables/file.json +47 -50
  113. package/tables/loginLog.json +68 -55
  114. package/tables/menu.json +20 -34
  115. package/tables/operateLog.json +53 -47
  116. package/tables/project.json +18 -30
  117. package/tables/role.json +33 -37
  118. package/utils/cors.js +37 -41
  119. package/utils/deepMerge.js +3 -1
  120. package/utils/error.js +1 -56
  121. package/utils/id.js +44 -0
  122. package/utils/is.js +6 -243
  123. package/utils/response.js +12 -1
  124. package/utils/scanFiles.js +13 -4
  125. package/utils/scanSources.js +58 -20
  126. package/utils/util.js +11 -163
  127. package/apis/_apis.js +0 -20
  128. package/apis/auth/sendSmsCode.js +0 -24
  129. package/apis/role/delete.js +0 -41
  130. package/apis/role/insert.js +0 -45
  131. package/apis/role/save.js +0 -36
  132. package/apis/role/update.js +0 -66
  133. package/configs/regexpAlias.json +0 -55
  134. package/lib/connect.js +0 -119
  135. package/lib/logger.js +0 -430
  136. package/lib/redisHelper.js +0 -399
  137. package/lib/validator.js +0 -274
  138. package/lib/xmlParse.js +0 -149
  139. package/plugins/config.js +0 -13
  140. package/plugins/cron.js +0 -115
  141. package/plugins/logger.js +0 -17
  142. package/plugins/tool.js +0 -131
  143. package/plugins/xmlParse.js +0 -10
  144. package/schemas/config.js +0 -67
  145. package/scripts/syncDb/context.js +0 -120
  146. package/scripts/syncDb/diff.js +0 -279
  147. package/scripts/syncDb/transform.js +0 -113
  148. package/sql/befly.sql +0 -271
  149. package/utils/email.js +0 -3
  150. package/utils/fieldClear.js +0 -22
  151. package/utils/formatValidationIssues.js +0 -28
  152. package/utils/getClientIp.js +0 -47
  153. package/utils/loggerUtils.js +0 -196
  154. package/utils/regexpUtil.js +0 -52
  155. /package/{lib → libs}/smtpText.js +0 -0
package/libs/cron.js ADDED
@@ -0,0 +1,60 @@
1
+ import { Logger } from "#befly/libs/logger/index.js";
2
+
3
+ import { isPrimaryProcess } from "../utils/is.js";
4
+
5
+ export function createCron(context) {
6
+ const jobs = new Map();
7
+
8
+ function register(name, schedule, task) {
9
+ if (!isPrimaryProcess()) return null;
10
+ if (jobs.has(name)) throw new Error(`定时任务 ${name} 重复注册`);
11
+
12
+ const job = Bun.cron(
13
+ schedule,
14
+ async () => {
15
+ Logger.info(`[定时任务] ${name} 开始执行`);
16
+ try {
17
+ await task(context);
18
+ Logger.info(`[定时任务] ${name} 执行完成`);
19
+ } catch (error) {
20
+ Logger.error(`[定时任务] ${name} 执行失败`, error);
21
+ }
22
+ },
23
+ context.config.tz ? { tz: context.config.tz } : undefined
24
+ );
25
+ jobs.set(name, job);
26
+ return job;
27
+ }
28
+
29
+ function second(name, seconds, task) {
30
+ if (!isPrimaryProcess()) return null;
31
+ if (jobs.has(name)) throw new Error(`定时任务 ${name} 重复注册`);
32
+
33
+ let running = false;
34
+ const timer = setInterval(async () => {
35
+ if (running) return;
36
+ running = true;
37
+ try {
38
+ await task(context);
39
+ } catch (error) {
40
+ Logger.error(`[定时任务] ${name} 执行失败`, error);
41
+ } finally {
42
+ running = false;
43
+ }
44
+ }, seconds * 1000);
45
+ const job = { stop: () => clearInterval(timer) };
46
+ jobs.set(name, job);
47
+ return job;
48
+ }
49
+
50
+ function stopAll() {
51
+ for (const job of jobs.values()) job.stop();
52
+ jobs.clear();
53
+ }
54
+
55
+ return { register: register, second: second, stopAll: stopAll };
56
+ }
57
+
58
+ export function registerCorns(context, corns) {
59
+ for (const corn of corns) context.cron.register(corn.fileName, corn.schedule, corn.handler);
60
+ }
@@ -0,0 +1 @@
1
+ export { Logger } from "./logger.js";
@@ -0,0 +1,245 @@
1
+ import { createWriteStream, existsSync, mkdirSync, rmSync } from "node:fs";
2
+ import { resolve } from "node:path";
3
+
4
+ import { buildSensitiveKeyMatcher, isPlainObject, sanitizeLogRecord } from "./sanitize.js";
5
+
6
+ const builtinSensitiveKeys = ["*password*", "pass", "pwd", "*token*", "access_token", "refresh_token", "accessToken", "refreshToken", "authorization", "cookie", "set-cookie", "*secret*", "apiKey", "api_key", "privateKey", "private_key"];
7
+ const textEncoder = new TextEncoder();
8
+
9
+ let config;
10
+ let sanitizeOptions;
11
+ let mockInstance = null;
12
+ let appSink = null;
13
+ let errorSink = null;
14
+
15
+ function normalizeInteger(value, fallback, min, max) {
16
+ if (!Number.isInteger(value)) return fallback;
17
+ return Math.min(max, Math.max(min, value));
18
+ }
19
+
20
+ function formatTime(value, dateOnly = false) {
21
+ const date = new Date(value);
22
+ const year = date.getFullYear();
23
+ const month = String(date.getMonth() + 1).padStart(2, "0");
24
+ const day = String(date.getDate()).padStart(2, "0");
25
+ if (dateOnly) return `${year}-${month}-${day}`;
26
+ const hour = String(date.getHours()).padStart(2, "0");
27
+ const minute = String(date.getMinutes()).padStart(2, "0");
28
+ const second = String(date.getSeconds()).padStart(2, "0");
29
+ return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
30
+ }
31
+
32
+ function writeStderr(message) {
33
+ Bun.stderr.write(`[befly-logger] ${message}\n`);
34
+ }
35
+
36
+ class FileSink {
37
+ constructor(prefix, sinkConfig) {
38
+ this.prefix = prefix;
39
+ this.config = sinkConfig;
40
+ this.stream = null;
41
+ this.date = "";
42
+ this.index = 0;
43
+ this.size = 0;
44
+ this.pending = Promise.resolve();
45
+ this.disabled = false;
46
+ }
47
+
48
+ getPath(date, index) {
49
+ if (this.prefix === "dev") return resolve(this.config.dir, "dev.log");
50
+ const instance = this.config.instanceId ? `.${this.config.instanceId}` : "";
51
+ const sequence = index > 0 ? `.${index}` : "";
52
+ const name = this.prefix === "app" ? `${date}${instance}${sequence}.log` : `${this.prefix}.${date}${instance}${sequence}.log`;
53
+ return resolve(this.config.dir, name);
54
+ }
55
+
56
+ open(path) {
57
+ this.stream = createWriteStream(path, { flags: "a" });
58
+ this.stream.on("error", (error) => {
59
+ this.disabled = true;
60
+ writeStderr(`写入失败 (${this.prefix}): ${error.message || error}`);
61
+ });
62
+ }
63
+
64
+ close(resetPosition = true) {
65
+ const stream = this.stream;
66
+ this.stream = null;
67
+ if (resetPosition) {
68
+ this.date = "";
69
+ this.index = 0;
70
+ this.size = 0;
71
+ }
72
+ if (!stream) return Promise.resolve();
73
+ return new Promise((resolveClose) => {
74
+ stream.end(resolveClose);
75
+ });
76
+ }
77
+
78
+ async prepare(bytes) {
79
+ const date = formatTime(Date.now(), true);
80
+ if (this.stream && this.date !== date) await this.close();
81
+
82
+ if (!this.stream) {
83
+ this.date = date;
84
+ this.index = 0;
85
+ let path = this.getPath(date, this.index);
86
+ this.size = Bun.file(path).size;
87
+ while (this.prefix !== "dev" && this.size > 0 && this.size + bytes > this.config.maxBytes) {
88
+ this.index += 1;
89
+ path = this.getPath(date, this.index);
90
+ this.size = Bun.file(path).size;
91
+ }
92
+ this.open(path);
93
+ }
94
+
95
+ if (this.prefix !== "dev" && this.size > 0 && this.size + bytes > this.config.maxBytes) {
96
+ await this.close(false);
97
+ this.index += 1;
98
+ this.size = Bun.file(this.getPath(date, this.index)).size;
99
+ this.open(this.getPath(date, this.index));
100
+ }
101
+ }
102
+
103
+ write(line) {
104
+ if (this.disabled) return;
105
+ const bytes = textEncoder.encode(line).byteLength;
106
+ this.pending = this.pending.then(async () => {
107
+ await this.prepare(bytes);
108
+ if (!this.stream || this.disabled) return;
109
+ this.size += bytes;
110
+ await new Promise((resolveWrite) => {
111
+ this.stream.write(line, resolveWrite);
112
+ });
113
+ });
114
+ }
115
+
116
+ async flush() {
117
+ await this.pending;
118
+ }
119
+
120
+ async shutdown() {
121
+ await this.pending;
122
+ await this.close();
123
+ }
124
+ }
125
+
126
+ function resetConfig(options = {}) {
127
+ const runtimeEnv = options.runtimeEnv === "development" ? "development" : "production";
128
+ const dir = options.dir ? resolve(options.dir) : resolve(process.cwd(), "logs");
129
+ const maxSize = normalizeInteger(options.maxSize, 20, 10, 100);
130
+
131
+ config = {
132
+ debug: options.debug ?? 0,
133
+ dir: dir,
134
+ runtimeEnv: runtimeEnv,
135
+ instanceId: Bun.env.BM2_INSTANCE_ID || Bun.env.BM2_APP_INSTANCE || "",
136
+ maxBytes: maxSize * 1024 * 1024
137
+ };
138
+
139
+ sanitizeOptions = {
140
+ sanitizeDepth: normalizeInteger(options.sanitizeDepth, 5, 1, 10),
141
+ sanitizeNodes: normalizeInteger(options.sanitizeNodes, 5000, 50, 20000),
142
+ sanitizeObjectKeys: normalizeInteger(options.sanitizeObjectKeys, 500, 10, 5000),
143
+ sensitiveKeyMatcher: buildSensitiveKeyMatcher(builtinSensitiveKeys, options.excludeFields)
144
+ };
145
+ }
146
+
147
+ function prepareDirectory(clearDevelopmentLog) {
148
+ mkdirSync(config.dir, { recursive: true });
149
+ if (clearDevelopmentLog && config.runtimeEnv === "development") {
150
+ const path = resolve(config.dir, "dev.log");
151
+ if (existsSync(path)) rmSync(path, { force: true });
152
+ }
153
+ }
154
+
155
+ function getSink(level) {
156
+ prepareDirectory(false);
157
+ if (config.runtimeEnv === "development") {
158
+ if (!appSink) appSink = new FileSink("dev", config);
159
+ return appSink;
160
+ }
161
+ if (level === "error") {
162
+ if (!errorSink) errorSink = new FileSink("error", config);
163
+ return errorSink;
164
+ }
165
+ if (!appSink) appSink = new FileSink("app", config);
166
+ return appSink;
167
+ }
168
+
169
+ function toRecord(input) {
170
+ if (isPlainObject(input)) return input;
171
+ if (input instanceof Error) return { msg: input.message || input.name || "Error", err: input };
172
+ if (input === undefined) return { msg: "" };
173
+ if (input === null) return { msg: "null" };
174
+ if (typeof input === "object") return { value: input };
175
+ try {
176
+ return { msg: String(input) };
177
+ } catch {
178
+ return { msg: "[Unserializable]" };
179
+ }
180
+ }
181
+
182
+ function buildLine(level, record) {
183
+ const output = {
184
+ level: level,
185
+ time: formatTime(Date.now()),
186
+ pid: process.pid
187
+ };
188
+ for (const [key, value] of Object.entries(record)) {
189
+ if (!["level", "time", "pid"].includes(key)) output[key] = value;
190
+ }
191
+ if (output.msg === undefined) output.msg = "";
192
+ try {
193
+ return `${JSON.stringify(output)}\n`;
194
+ } catch {
195
+ return `${JSON.stringify({ level: level, time: output.time, pid: process.pid, msg: "[Unserializable log record]" })}\n`;
196
+ }
197
+ }
198
+
199
+ function write(level, input) {
200
+ if (level === "debug" && config.debug !== 1 && config.debug !== true) return;
201
+ const record = sanitizeLogRecord(toRecord(input), sanitizeOptions);
202
+ if (mockInstance) {
203
+ mockInstance[level](record);
204
+ return;
205
+ }
206
+ getSink(level).write(buildLine(level, record));
207
+ }
208
+
209
+ function createMethod(level) {
210
+ return function (message, data) {
211
+ write(level, isPlainObject(message) ? message : { msg: message, data: data });
212
+ };
213
+ }
214
+
215
+ resetConfig();
216
+
217
+ export const Logger = {
218
+ info: createMethod("info"),
219
+ warn: createMethod("warn"),
220
+ error: function (message, error, data) {
221
+ write("error", isPlainObject(message) ? message : { msg: message, err: error, data: data });
222
+ },
223
+ debug: createMethod("debug"),
224
+ configure: function (options = {}) {
225
+ const sinks = [appSink, errorSink].filter(Boolean);
226
+ appSink = null;
227
+ errorSink = null;
228
+ for (const sink of sinks) sink.shutdown().catch((error) => writeStderr(`关闭失败: ${error.message || error}`));
229
+ resetConfig(options);
230
+ prepareDirectory(true);
231
+ },
232
+ setMock: function (mock) {
233
+ mockInstance = mock;
234
+ },
235
+ flush: async function () {
236
+ if (mockInstance) return;
237
+ await Promise.all([appSink?.flush(), errorSink?.flush()]);
238
+ },
239
+ shutdown: async function () {
240
+ const sinks = [appSink, errorSink].filter(Boolean);
241
+ appSink = null;
242
+ errorSink = null;
243
+ await Promise.all(sinks.map((sink) => sink.shutdown()));
244
+ }
245
+ };
@@ -0,0 +1,115 @@
1
+ function isPlainObject(value) {
2
+ if (typeof value !== "object" || value === null) return false;
3
+ const prototype = Object.getPrototypeOf(value);
4
+ return prototype === Object.prototype || prototype === null;
5
+ }
6
+
7
+ function errorChainSummary(error) {
8
+ const parts = [];
9
+ const visited = new Set();
10
+ let current = error;
11
+
12
+ while (current instanceof Error && !visited.has(current)) {
13
+ visited.add(current);
14
+ const meta = [current.code, current.subsystem, current.operation].filter(Boolean).join("/");
15
+ const text = current.message || current.name || "Error";
16
+ parts.push(meta ? `${text} [${meta}]` : text);
17
+ current = current.cause;
18
+ }
19
+
20
+ return parts.join(" <- ");
21
+ }
22
+
23
+ export function buildSensitiveKeyMatcher(builtinPatterns, userPatterns) {
24
+ const exactSet = new Set();
25
+ const contains = [];
26
+
27
+ for (const item of [...builtinPatterns, ...(Array.isArray(userPatterns) ? userPatterns : [])]) {
28
+ const pattern = String(item).trim().toLowerCase();
29
+ if (!pattern) continue;
30
+ if (!pattern.includes("*")) {
31
+ exactSet.add(pattern);
32
+ continue;
33
+ }
34
+ const core = pattern.replace(/\*+/g, "").trim();
35
+ if (core) contains.push(core);
36
+ }
37
+
38
+ return { exactSet: exactSet, contains: contains };
39
+ }
40
+
41
+ function isSensitiveKey(key, matcher) {
42
+ const lower = String(key).toLowerCase();
43
+ if (matcher.exactSet.has(lower)) return true;
44
+ return matcher.contains.some((part) => lower.includes(part));
45
+ }
46
+
47
+ function sanitizeError(error, options, state, depth, visited) {
48
+ if (visited.has(error)) return "[Circular]";
49
+ visited.add(error);
50
+
51
+ const output = {
52
+ name: error.name || "Error",
53
+ message: error.message || "",
54
+ chain: errorChainSummary(error)
55
+ };
56
+
57
+ if (typeof error.stack === "string") output.stack = error.stack;
58
+ if (error.cause !== undefined) output.cause = sanitizeValue(error.cause, options, state, depth + 1, visited);
59
+
60
+ for (const key of Object.keys(error)) {
61
+ if (["name", "message", "stack", "cause"].includes(key)) continue;
62
+ output[key] = isSensitiveKey(key, options.sensitiveKeyMatcher) ? "[MASKED]" : sanitizeValue(error[key], options, state, depth + 1, visited);
63
+ }
64
+
65
+ return output;
66
+ }
67
+
68
+ function stringifyPreview(value, options) {
69
+ const seen = new WeakSet();
70
+ try {
71
+ return JSON.stringify(value, (key, item) => {
72
+ if (key && isSensitiveKey(key, options.sensitiveKeyMatcher)) return "[MASKED]";
73
+ if (typeof item === "bigint") return String(item);
74
+ if (item && typeof item === "object") {
75
+ if (seen.has(item)) return "[Circular]";
76
+ seen.add(item);
77
+ }
78
+ return item;
79
+ });
80
+ } catch {
81
+ try {
82
+ return String(value);
83
+ } catch {
84
+ return "[Unserializable]";
85
+ }
86
+ }
87
+ }
88
+
89
+ function sanitizeValue(value, options, state, depth, visited) {
90
+ if (value === null || value === undefined || typeof value === "string" || typeof value === "number" || typeof value === "boolean") return value;
91
+ if (typeof value === "bigint") return String(value);
92
+ if (value instanceof Error) return sanitizeError(value, options, state, depth, visited);
93
+
94
+ if (!Array.isArray(value) && !isPlainObject(value)) return stringifyPreview(value, options);
95
+ if (visited.has(value)) return "[Circular]";
96
+ if (depth >= options.sanitizeDepth || state.nodes >= options.sanitizeNodes) return stringifyPreview(value, options);
97
+
98
+ visited.add(value);
99
+ state.nodes += 1;
100
+
101
+ if (Array.isArray(value)) return value.map((item) => sanitizeValue(item, options, state, depth + 1, visited));
102
+
103
+ const output = {};
104
+ const entries = Object.entries(value).slice(0, options.sanitizeObjectKeys);
105
+ for (const [key, item] of entries) {
106
+ output[key] = isSensitiveKey(key, options.sensitiveKeyMatcher) ? "[MASKED]" : sanitizeValue(item, options, state, depth + 1, visited);
107
+ }
108
+ return output;
109
+ }
110
+
111
+ export function sanitizeLogRecord(record, options) {
112
+ return sanitizeValue(record, options, { nodes: 0 }, 0, new WeakSet());
113
+ }
114
+
115
+ export { isPlainObject };