befly 3.17.0 → 3.17.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 (150) hide show
  1. package/README.md +6 -0
  2. package/apis/admin/cacheRefresh.js +122 -0
  3. package/apis/admin/del.js +34 -0
  4. package/apis/admin/detail.js +23 -0
  5. package/apis/admin/ins.js +69 -0
  6. package/apis/admin/list.js +28 -0
  7. package/apis/admin/upd.js +95 -0
  8. package/apis/api/all.js +24 -0
  9. package/apis/api/list.js +31 -0
  10. package/apis/auth/login.js +123 -0
  11. package/apis/auth/sendSmsCode.js +24 -0
  12. package/apis/dashboard/configStatus.js +39 -0
  13. package/apis/dashboard/environmentInfo.js +43 -0
  14. package/apis/dashboard/performanceMetrics.js +20 -0
  15. package/apis/dashboard/permissionStats.js +27 -0
  16. package/apis/dashboard/serviceStatus.js +75 -0
  17. package/apis/dashboard/systemInfo.js +19 -0
  18. package/apis/dashboard/systemOverview.js +30 -0
  19. package/apis/dashboard/systemResources.js +106 -0
  20. package/apis/dict/all.js +23 -0
  21. package/apis/dict/del.js +16 -0
  22. package/apis/dict/detail.js +27 -0
  23. package/apis/dict/ins.js +51 -0
  24. package/apis/dict/items.js +30 -0
  25. package/apis/dict/list.js +36 -0
  26. package/apis/dict/upd.js +74 -0
  27. package/apis/dictType/all.js +16 -0
  28. package/apis/dictType/del.js +38 -0
  29. package/apis/dictType/detail.js +20 -0
  30. package/apis/dictType/ins.js +37 -0
  31. package/apis/dictType/list.js +26 -0
  32. package/apis/dictType/upd.js +51 -0
  33. package/apis/email/config.js +25 -0
  34. package/apis/email/logList.js +23 -0
  35. package/apis/email/send.js +66 -0
  36. package/apis/email/verify.js +21 -0
  37. package/apis/loginLog/list.js +23 -0
  38. package/apis/menu/all.js +41 -0
  39. package/apis/menu/list.js +25 -0
  40. package/apis/operateLog/list.js +23 -0
  41. package/apis/role/all.js +21 -0
  42. package/apis/role/apiSave.js +43 -0
  43. package/apis/role/apis.js +22 -0
  44. package/apis/role/del.js +49 -0
  45. package/apis/role/detail.js +32 -0
  46. package/apis/role/ins.js +46 -0
  47. package/apis/role/list.js +27 -0
  48. package/apis/role/menuSave.js +42 -0
  49. package/apis/role/menus.js +22 -0
  50. package/apis/role/save.js +40 -0
  51. package/apis/role/upd.js +50 -0
  52. package/apis/sysConfig/all.js +16 -0
  53. package/apis/sysConfig/del.js +36 -0
  54. package/apis/sysConfig/get.js +49 -0
  55. package/apis/sysConfig/ins.js +50 -0
  56. package/apis/sysConfig/list.js +24 -0
  57. package/apis/sysConfig/upd.js +62 -0
  58. package/checks/api.js +55 -0
  59. package/checks/config.js +107 -0
  60. package/checks/hook.js +38 -0
  61. package/checks/menu.js +58 -0
  62. package/checks/plugin.js +38 -0
  63. package/checks/table.js +78 -0
  64. package/configs/beflyConfig.json +61 -0
  65. package/configs/beflyMenus.json +85 -0
  66. package/configs/constConfig.js +34 -0
  67. package/configs/regexpAlias.json +55 -0
  68. package/hooks/auth.js +34 -0
  69. package/hooks/cors.js +39 -0
  70. package/hooks/parser.js +90 -0
  71. package/hooks/permission.js +71 -0
  72. package/hooks/validator.js +43 -0
  73. package/index.js +326 -0
  74. package/lib/cacheHelper.js +483 -0
  75. package/lib/cacheKeys.js +42 -0
  76. package/lib/connect.js +120 -0
  77. package/lib/dbHelper/builders.js +698 -0
  78. package/lib/dbHelper/context.js +131 -0
  79. package/lib/dbHelper/dataOps.js +505 -0
  80. package/lib/dbHelper/execute.js +65 -0
  81. package/lib/dbHelper/index.js +27 -0
  82. package/lib/dbHelper/transaction.js +43 -0
  83. package/lib/dbHelper/util.js +58 -0
  84. package/lib/dbHelper/validate.js +549 -0
  85. package/lib/emailHelper.js +110 -0
  86. package/lib/logger.js +604 -0
  87. package/lib/redisHelper.js +684 -0
  88. package/lib/sqlBuilder/batch.js +113 -0
  89. package/lib/sqlBuilder/check.js +150 -0
  90. package/lib/sqlBuilder/compiler.js +347 -0
  91. package/lib/sqlBuilder/errors.js +60 -0
  92. package/lib/sqlBuilder/index.js +218 -0
  93. package/lib/sqlBuilder/parser.js +296 -0
  94. package/lib/sqlBuilder/util.js +260 -0
  95. package/lib/validator.js +303 -0
  96. package/package.json +19 -12
  97. package/paths.js +112 -0
  98. package/plugins/cache.js +16 -0
  99. package/plugins/config.js +11 -0
  100. package/plugins/email.js +27 -0
  101. package/plugins/logger.js +20 -0
  102. package/plugins/mysql.js +36 -0
  103. package/plugins/redis.js +34 -0
  104. package/plugins/tool.js +155 -0
  105. package/router/api.js +140 -0
  106. package/router/static.js +71 -0
  107. package/sql/admin.sql +18 -0
  108. package/sql/api.sql +12 -0
  109. package/sql/dict.sql +13 -0
  110. package/sql/dictType.sql +12 -0
  111. package/sql/emailLog.sql +20 -0
  112. package/sql/loginLog.sql +25 -0
  113. package/sql/menu.sql +12 -0
  114. package/sql/operateLog.sql +22 -0
  115. package/sql/role.sql +14 -0
  116. package/sql/sysConfig.sql +16 -0
  117. package/sync/api.js +93 -0
  118. package/sync/cache.js +13 -0
  119. package/sync/dev.js +171 -0
  120. package/sync/menu.js +99 -0
  121. package/tables/admin.json +56 -0
  122. package/tables/api.json +26 -0
  123. package/tables/dict.json +30 -0
  124. package/tables/dictType.json +24 -0
  125. package/tables/emailLog.json +61 -0
  126. package/tables/loginLog.json +86 -0
  127. package/tables/menu.json +24 -0
  128. package/tables/operateLog.json +68 -0
  129. package/tables/role.json +32 -0
  130. package/tables/sysConfig.json +43 -0
  131. package/utils/calcPerfTime.js +13 -0
  132. package/utils/cors.js +17 -0
  133. package/utils/deepMerge.js +78 -0
  134. package/utils/fieldClear.js +65 -0
  135. package/utils/formatYmdHms.js +23 -0
  136. package/utils/formatZodIssues.js +109 -0
  137. package/utils/getClientIp.js +47 -0
  138. package/utils/importDefault.js +51 -0
  139. package/utils/is.js +462 -0
  140. package/utils/loggerUtils.js +185 -0
  141. package/utils/processInfo.js +39 -0
  142. package/utils/regexpUtil.js +52 -0
  143. package/utils/response.js +114 -0
  144. package/utils/scanFiles.js +124 -0
  145. package/utils/scanSources.js +68 -0
  146. package/utils/sortModules.js +75 -0
  147. package/utils/toSessionTtlSeconds.js +14 -0
  148. package/utils/util.js +374 -0
  149. package/befly.js +0 -16413
  150. package/befly.min.js +0 -72
@@ -0,0 +1,51 @@
1
+ /**
2
+ * 动态导入模块并优先返回其 default 导出。
3
+ *
4
+ * - import() 报错:返回 defaultValue
5
+ * - default 导出为 null/undefined:返回 defaultValue
6
+ */
7
+ import { isAbsolute } from "node:path";
8
+ import { pathToFileURL } from "node:url";
9
+
10
+ import { Logger } from "../lib/logger.js";
11
+ import { isNullable } from "./is.js";
12
+ import { getTypeTag } from "./util.js";
13
+
14
+ function isWindowsAbsPath(file) {
15
+ // e.g. C:\a\b 或 C:/a/b
16
+ return /^[a-zA-Z]:[\\/]/.test(file);
17
+ }
18
+
19
+ function toFileImportUrl(file) {
20
+ if (isAbsolute(file) || isWindowsAbsPath(file)) {
21
+ return pathToFileURL(file).href;
22
+ }
23
+ return file;
24
+ }
25
+
26
+ export async function importDefault(file, defaultValue) {
27
+ try {
28
+ const isJson = file.endsWith(".json");
29
+
30
+ // 注意:Node ESM / Bun 对 JSON import 的行为不同。
31
+ // 为保证一致性:json 一律走 import assertion。
32
+ const mod = isJson ? await import(toFileImportUrl(file), { with: { type: "json" } }) : await import(file);
33
+ const value = mod?.default;
34
+ if (isNullable(value)) {
35
+ return defaultValue;
36
+ }
37
+
38
+ // 额外保护:如果导入值的运行时类型与 defaultValue 不一致,则回退 defaultValue。
39
+ // 这样调用方可以通过 defaultValue 的类型约束导入结果(例如 defaultValue 为 {} 时,数组/字符串等会被拒绝)。
40
+ const expectedType = getTypeTag(defaultValue);
41
+ const actualType = getTypeTag(value);
42
+ if (expectedType !== actualType) {
43
+ Logger.warn("importDefault 导入类型与默认值不一致,已回退到默认值", { file: file, expectedType: expectedType, actualType: actualType });
44
+ return defaultValue;
45
+ }
46
+ return value;
47
+ } catch (err) {
48
+ Logger.warn("importDefault 导入失败,已回退到默认值", { err: err, file: file });
49
+ return defaultValue;
50
+ }
51
+ }
package/utils/is.js ADDED
@@ -0,0 +1,462 @@
1
+ import { statSync } from "node:fs";
2
+
3
+ import { join } from "pathe";
4
+
5
+ import { DECIMAL_KIND_TYPES, ENUM_KIND_TYPES, FLOAT_KIND_TYPES, INT_KIND_TYPES, JSON_KIND_TYPES, STRING_KIND_TYPES, TEXT_KIND_TYPES } from "../configs/constConfig.js";
6
+
7
+ /**
8
+ * 判断值是否为 plain object(原型为 Object.prototype 或 null)。
9
+ */
10
+ export function isPlainObject(value) {
11
+ if (typeof value !== "object" || value === null) {
12
+ return false;
13
+ }
14
+
15
+ const proto = Object.getPrototypeOf(value);
16
+ return proto === Object.prototype || proto === null;
17
+ }
18
+
19
+ /**
20
+ * 判断值是否为 JSON 基础类型(string/number/boolean/null)。
21
+ */
22
+ export function isJsonPrimitive(value) {
23
+ if (value === null) {
24
+ return true;
25
+ }
26
+ return typeof value === "string" || isNumber(value) || isBoolean(value);
27
+ }
28
+
29
+ /**
30
+ * 判断值是否为 JSON 对象(plain object,且非数组/非 Date)。
31
+ */
32
+ export function isJsonObject(value) {
33
+ if (!value || typeof value !== "object") {
34
+ return false;
35
+ }
36
+ if (value instanceof Date) {
37
+ return false;
38
+ }
39
+ if (Array.isArray(value)) {
40
+ return false;
41
+ }
42
+ return true;
43
+ }
44
+
45
+ /**
46
+ * 判断值是否为 JSON 结构(数组或对象)。
47
+ */
48
+ export function isJsonStructure(value) {
49
+ return Array.isArray(value) || isJsonObject(value);
50
+ }
51
+
52
+ /**
53
+ * 判断值是否为可 JSON 序列化结构(递归)。
54
+ */
55
+ export function isJsonValue(value) {
56
+ if (isJsonPrimitive(value)) {
57
+ return true;
58
+ }
59
+
60
+ if (Array.isArray(value)) {
61
+ for (const item of value) {
62
+ if (!isJsonValue(item)) {
63
+ return false;
64
+ }
65
+ }
66
+ return true;
67
+ }
68
+
69
+ if (isJsonObject(value)) {
70
+ for (const v of Object.values(value)) {
71
+ if (v === undefined) {
72
+ continue;
73
+ }
74
+ if (!isJsonValue(v)) {
75
+ return false;
76
+ }
77
+ }
78
+ return true;
79
+ }
80
+
81
+ return false;
82
+ }
83
+
84
+ /**
85
+ * 判断值是否为有限数值。
86
+ */
87
+ export function isFiniteNumber(value) {
88
+ return isNumber(value) && Number.isFinite(value);
89
+ }
90
+
91
+ /**
92
+ * 判断值是否为字符串。
93
+ */
94
+ export function isString(value) {
95
+ return typeof value === "string";
96
+ }
97
+
98
+ /**
99
+ * 判断值是否为 number。
100
+ */
101
+ export function isNumber(value) {
102
+ return Object.prototype.toString.call(value) === "[object Number]";
103
+ }
104
+
105
+ /**
106
+ * 判断值是否为 boolean。
107
+ */
108
+ export function isBoolean(value) {
109
+ return Object.prototype.toString.call(value) === "[object Boolean]";
110
+ }
111
+
112
+ /**
113
+ * 判断值是否为 function。
114
+ */
115
+ export function isFunction(value) {
116
+ return Object.prototype.toString.call(value) === "[object Function]";
117
+ }
118
+
119
+ /**
120
+ * 判断值是否为 nullable(默认仅 null/undefined)。
121
+ * 可通过 extraValues 指定更多“空值”。
122
+ */
123
+ export function isNullable(value, extraValues = []) {
124
+ if (value === null || value === undefined) {
125
+ return true;
126
+ }
127
+
128
+ if (!Array.isArray(extraValues) || extraValues.length === 0) {
129
+ return false;
130
+ }
131
+
132
+ for (const item of extraValues) {
133
+ if (Object.is(value, item)) {
134
+ return true;
135
+ }
136
+ }
137
+
138
+ return false;
139
+ }
140
+
141
+ /**
142
+ * 判断字符串长度是否至少为指定值(trim 后)。
143
+ */
144
+ export function isMinLengthString(value, minLength) {
145
+ if (typeof value !== "string") {
146
+ return false;
147
+ }
148
+ if (!Number.isFinite(minLength)) {
149
+ return false;
150
+ }
151
+ if (minLength <= 0) {
152
+ return true;
153
+ }
154
+ return value.trim().length >= minLength;
155
+ }
156
+
157
+ /**
158
+ * 判断值是否为非空字符串。
159
+ */
160
+ export function isNonEmptyString(value) {
161
+ return typeof value === "string" && value.trim().length > 0;
162
+ }
163
+
164
+ /**
165
+ * 判断字符串是否允许空字符串且不允许首尾空白。
166
+ */
167
+ export function isNoTrimStringAllowEmpty(value) {
168
+ if (typeof value !== "string") {
169
+ return false;
170
+ }
171
+
172
+ if (value.length === 0) {
173
+ return true;
174
+ }
175
+
176
+ const firstCharCode = value.charCodeAt(0);
177
+ if (isWhitespaceCharCode(firstCharCode)) {
178
+ return false;
179
+ }
180
+
181
+ const lastCharCode = value.charCodeAt(value.length - 1);
182
+ if (isWhitespaceCharCode(lastCharCode)) {
183
+ return false;
184
+ }
185
+
186
+ return true;
187
+ }
188
+
189
+ function isWhitespaceCharCode(code) {
190
+ if (code === 9 || code === 10 || code === 11 || code === 12 || code === 13) {
191
+ return true;
192
+ }
193
+ if (code === 32 || code === 133 || code === 160) {
194
+ return true;
195
+ }
196
+ if (code === 5760 || code === 8232 || code === 8233 || code === 8239 || code === 8287 || code === 12288 || code === 65279) {
197
+ return true;
198
+ }
199
+ if (code >= 8192 && code <= 8202) {
200
+ return true;
201
+ }
202
+ return false;
203
+ }
204
+
205
+ /**
206
+ * 判断是否为小驼峰命名(可选以下划线开头)。
207
+ */
208
+ export function isLowerCamelCaseName(value) {
209
+ if (!isNonEmptyString(value)) {
210
+ return false;
211
+ }
212
+ return /^_?[a-z][a-z0-9]*(?:[A-Z][a-z0-9]*)*$/.test(value);
213
+ }
214
+
215
+ /**
216
+ * 判断值是否为合法端口号(1..65535)。
217
+ */
218
+ export function isValidPort(value) {
219
+ return isFiniteNumber(value) && value >= 1 && value <= 65535;
220
+ }
221
+
222
+ /**
223
+ * 判断值是否为合法非负整数。
224
+ */
225
+ export function isValidNonNegativeInt(value) {
226
+ return Number.isInteger(value) && value >= 0;
227
+ }
228
+
229
+ /**
230
+ * 判断值是否为合法正整数。
231
+ */
232
+ export function isValidPositiveInt(value) {
233
+ return Number.isInteger(value) && value >= 1;
234
+ }
235
+
236
+ /**
237
+ * 判断值是否为有效 IANA 时区字符串。
238
+ */
239
+ export function isValidTimeZone(value) {
240
+ if (!isNonEmptyString(value)) return false;
241
+
242
+ try {
243
+ // RangeError: Invalid time zone specified
244
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format(0);
245
+ return true;
246
+ } catch {
247
+ return false;
248
+ }
249
+ }
250
+
251
+ /**
252
+ * 判断值是否为有限整数。
253
+ */
254
+ export function isIntegerNumber(value) {
255
+ return isNumber(value) && Number.isInteger(value) && Number.isFinite(value);
256
+ }
257
+
258
+ /**
259
+ * 判断字符串是否为正则字面量(/source/flags)。
260
+ */
261
+ export function isRegexLiteral(input) {
262
+ const trimmed = String(input || "").trim();
263
+ if (!isMinLengthString(trimmed, 2)) {
264
+ return false;
265
+ }
266
+ if (!trimmed.startsWith("/")) {
267
+ return false;
268
+ }
269
+
270
+ const lastSlash = trimmed.lastIndexOf("/");
271
+ if (lastSlash <= 0) {
272
+ return false;
273
+ }
274
+
275
+ const flags = trimmed.slice(lastSlash + 1);
276
+ if (flags !== "" && !/^[gimsuy]+$/.test(flags)) {
277
+ return false;
278
+ }
279
+
280
+ return true;
281
+ }
282
+
283
+ /**
284
+ * 判断输入是否为正则规则(字面量或 @ 别名)。
285
+ */
286
+ export function isRegexInput(input) {
287
+ const trimmed = String(input || "").trim();
288
+ // 约束:正则 input 仅允许 /source/flags 形式;同时保留 @alias 作为正则别名。
289
+ // 说明:不再支持直接写 source(如 ^\d+$),避免与普通字符串/枚举产生歧义。
290
+ return isRegexLiteral(trimmed) || trimmed.startsWith("@");
291
+ }
292
+
293
+ /**
294
+ * 判断输入是否为枚举规则(首字符非 / 且包含 |)。
295
+ */
296
+ export function isEnumInput(input) {
297
+ const trimmed = String(input || "").trim();
298
+ if (trimmed.length === 0) {
299
+ return false;
300
+ }
301
+ return !trimmed.startsWith("/") && trimmed.includes("|");
302
+ }
303
+
304
+ /**
305
+ * 判断输入是否为规则别名(@ 开头)。
306
+ */
307
+ export function isAliasInput(input) {
308
+ return String(input || "")
309
+ .trim()
310
+ .startsWith("@");
311
+ }
312
+
313
+ /**
314
+ * 激进空值判断(项目约定)。
315
+ */
316
+ export function isEmpty(value) {
317
+ if (isNullable(value)) {
318
+ return true;
319
+ }
320
+
321
+ if (typeof value === "string") {
322
+ return value.trim().length === 0;
323
+ }
324
+
325
+ if (isNumber(value)) {
326
+ return value === 0 || Number.isNaN(value);
327
+ }
328
+
329
+ if (isBoolean(value)) {
330
+ return value === false;
331
+ }
332
+
333
+ if (Array.isArray(value)) {
334
+ return value.length === 0;
335
+ }
336
+
337
+ if (value instanceof Map || value instanceof Set) {
338
+ return value.size === 0;
339
+ }
340
+
341
+ if (isPlainObject(value)) {
342
+ return Object.keys(value).length === 0;
343
+ }
344
+
345
+ return false;
346
+ }
347
+
348
+ /**
349
+ * 判断当前进程是否为主进程(针对 PM2 cluster 常见环境变量)。
350
+ */
351
+ export function isPrimaryProcess(env) {
352
+ const sourceEnv = env && typeof env === "object" ? env : process.env;
353
+
354
+ const bunWorkerId = sourceEnv.BUN_WORKER_ID;
355
+ if (bunWorkerId !== undefined && bunWorkerId !== null) {
356
+ const normalizedBunWorkerId = String(bunWorkerId).trim();
357
+ if (normalizedBunWorkerId === "" || normalizedBunWorkerId === "0") {
358
+ return true;
359
+ }
360
+ const bunWorkerIdNumber = Number(normalizedBunWorkerId);
361
+ if (Number.isInteger(bunWorkerIdNumber) && bunWorkerIdNumber >= 0) {
362
+ return bunWorkerIdNumber === 0;
363
+ }
364
+ return false;
365
+ }
366
+
367
+ const pm2InstanceIdRaw = sourceEnv.NODE_APP_INSTANCE ?? sourceEnv.PM2_INSTANCE_ID ?? sourceEnv.pm_id;
368
+ if (pm2InstanceIdRaw !== undefined && pm2InstanceIdRaw !== null) {
369
+ const normalizedPm2InstanceId = String(pm2InstanceIdRaw).trim();
370
+ if (normalizedPm2InstanceId === "0") {
371
+ return true;
372
+ }
373
+ const pm2InstanceIdNumber = Number(normalizedPm2InstanceId);
374
+ if (Number.isInteger(pm2InstanceIdNumber) && pm2InstanceIdNumber >= 0) {
375
+ return pm2InstanceIdNumber === 0;
376
+ }
377
+ return false;
378
+ }
379
+
380
+ const execMode = String(sourceEnv.exec_mode || sourceEnv.NODE_EXEC_MODE || "").toLowerCase();
381
+ const hasPm2Markers = sourceEnv.PM2_HOME !== undefined || sourceEnv.pm_exec_path !== undefined || sourceEnv.PM2_USAGE !== undefined || execMode.includes("cluster");
382
+ if (hasPm2Markers) {
383
+ return false;
384
+ }
385
+
386
+ return true;
387
+ }
388
+
389
+ export const isDirentDirectory = (parentDir, entry) => {
390
+ if (entry.isDirectory()) {
391
+ return true;
392
+ }
393
+
394
+ // 兼容 Windows 下的 junction / workspace link:Dirent.isDirectory() 可能为 false,但它实际指向目录。
395
+ if (!entry.isSymbolicLink()) {
396
+ return false;
397
+ }
398
+
399
+ try {
400
+ const stats = statSync(join(parentDir, entry.name));
401
+ return stats.isDirectory();
402
+ } catch {
403
+ return false;
404
+ }
405
+ };
406
+
407
+ /**
408
+ * 数据库类型判定
409
+ */
410
+ export function isStringDbType(dbType) {
411
+ return STRING_KIND_TYPES.includes(dbType);
412
+ }
413
+
414
+ export function isTextDbType(dbType) {
415
+ return TEXT_KIND_TYPES.includes(dbType);
416
+ }
417
+
418
+ export function isIntDbType(dbType) {
419
+ return INT_KIND_TYPES.includes(dbType);
420
+ }
421
+
422
+ export function isDecimalDbType(dbType) {
423
+ return DECIMAL_KIND_TYPES.includes(dbType);
424
+ }
425
+
426
+ export function isFloatDbType(dbType) {
427
+ return FLOAT_KIND_TYPES.includes(dbType);
428
+ }
429
+
430
+ export function isJsonDbType(dbType) {
431
+ return JSON_KIND_TYPES.includes(dbType);
432
+ }
433
+
434
+ export function isEnumDbType(dbType) {
435
+ return ENUM_KIND_TYPES.includes(dbType);
436
+ }
437
+
438
+ export function isNumericDbType(dbType) {
439
+ return INT_KIND_TYPES.includes(dbType) || DECIMAL_KIND_TYPES.includes(dbType) || FLOAT_KIND_TYPES.includes(dbType);
440
+ }
441
+
442
+ /**
443
+ * SQL Value 判定
444
+ */
445
+ export function isSqlValue(value) {
446
+ if (value instanceof Date) return true;
447
+ return isJsonValue(value);
448
+ }
449
+
450
+ /**
451
+ * 日志敏感键判定
452
+ */
453
+ export function isSensitiveKey(key, matcher) {
454
+ const lower = String(key).toLowerCase();
455
+ if (matcher.exactSet.has(lower)) return true;
456
+
457
+ for (const part of matcher.contains) {
458
+ if (lower.includes(part)) return true;
459
+ }
460
+
461
+ return false;
462
+ }
@@ -0,0 +1,185 @@
1
+ import { isBoolean, isNullable, isNumber, isPlainObject, isSensitiveKey, isString } from "./is.js";
2
+
3
+ export function buildSensitiveKeyMatcher(options) {
4
+ const exactSet = new Set();
5
+ const contains = [];
6
+
7
+ const addPattern = (item) => {
8
+ const trimmed = String(item).trim().toLowerCase();
9
+ if (trimmed.length === 0) return;
10
+
11
+ if (!trimmed.includes("*")) {
12
+ exactSet.add(trimmed);
13
+ return;
14
+ }
15
+
16
+ const core = trimmed.replace(/\*+/g, "").trim();
17
+ if (!core) return;
18
+ contains.push(core);
19
+ };
20
+
21
+ for (const item of options.builtinPatterns) {
22
+ addPattern(item);
23
+ }
24
+
25
+ if (Array.isArray(options.userPatterns)) {
26
+ for (const item of options.userPatterns) {
27
+ addPattern(item);
28
+ }
29
+ }
30
+
31
+ return { exactSet: exactSet, contains: contains };
32
+ }
33
+
34
+ function truncateString(value, maxLen) {
35
+ if (value.length <= maxLen) return value;
36
+ return value.slice(0, maxLen);
37
+ }
38
+
39
+ function sanitizeErrorValue(err, options) {
40
+ const out = {
41
+ name: err.name || "Error",
42
+ message: truncateString(err.message || "", options.maxStringLen)
43
+ };
44
+
45
+ if (isString(err.stack)) {
46
+ out["stack"] = truncateString(err.stack, options.maxStringLen);
47
+ }
48
+
49
+ return out;
50
+ }
51
+
52
+ function safeToStringMasked(value, options, visited) {
53
+ if (isString(value)) return truncateString(value, options.maxStringLen);
54
+
55
+ if (value instanceof Error) {
56
+ try {
57
+ return truncateString(JSON.stringify(sanitizeErrorValue(value, options)), options.maxStringLen);
58
+ } catch {
59
+ return truncateString(`${value.name || "Error"}: ${value.message || ""}`, options.maxStringLen);
60
+ }
61
+ }
62
+
63
+ if (value && typeof value === "object") {
64
+ if (visited.has(value)) {
65
+ return "[Circular]";
66
+ }
67
+ }
68
+
69
+ try {
70
+ const localVisited = visited;
71
+ const replacer = (k, v) => {
72
+ if (k && isSensitiveKey(k, options.sensitiveKeyMatcher)) {
73
+ return "[MASKED]";
74
+ }
75
+
76
+ if (v && typeof v === "object") {
77
+ if (localVisited.has(v)) {
78
+ return "[Circular]";
79
+ }
80
+ localVisited.add(v);
81
+ }
82
+
83
+ return v;
84
+ };
85
+
86
+ return truncateString(JSON.stringify(value, replacer), options.maxStringLen);
87
+ } catch {
88
+ try {
89
+ return truncateString(String(value), options.maxStringLen);
90
+ } catch {
91
+ return "[Unserializable]";
92
+ }
93
+ }
94
+ }
95
+
96
+ function sanitizeAny(value, options, state, depth, visited) {
97
+ if (isNullable(value)) return value;
98
+
99
+ if (isString(value)) {
100
+ return truncateString(value, options.maxStringLen);
101
+ }
102
+
103
+ if (isNumber(value) || isBoolean(value) || typeof value === "bigint") {
104
+ return value;
105
+ }
106
+
107
+ if (value instanceof Error) {
108
+ return sanitizeErrorValue(value, options);
109
+ }
110
+
111
+ const isArr = Array.isArray(value);
112
+ const isObj = isPlainObject(value);
113
+
114
+ if (!isArr && !isObj) {
115
+ return safeToStringMasked(value, options, visited);
116
+ }
117
+
118
+ if (visited.has(value)) {
119
+ return "[Circular]";
120
+ }
121
+
122
+ if (depth >= options.sanitizeDepth) {
123
+ return safeToStringMasked(value, options, visited);
124
+ }
125
+
126
+ if (state.nodes >= options.sanitizeNodes) {
127
+ return safeToStringMasked(value, options, visited);
128
+ }
129
+
130
+ visited.add(value);
131
+ state.nodes = state.nodes + 1;
132
+
133
+ if (isArr) {
134
+ const arr = value;
135
+ const out = [];
136
+
137
+ const limit = arr.length > options.maxArrayItems ? options.maxArrayItems : arr.length;
138
+ for (let i = 0; i < limit; i++) {
139
+ out[i] = sanitizeAny(arr[i], options, state, depth + 1, visited);
140
+ }
141
+
142
+ return out;
143
+ }
144
+
145
+ const obj = value;
146
+ const out = {};
147
+
148
+ const entries = Object.entries(obj);
149
+ const limit = entries.length > options.sanitizeObjectKeys ? options.sanitizeObjectKeys : entries.length;
150
+
151
+ for (let i = 0; i < limit; i++) {
152
+ const entry = entries[i];
153
+ if (!entry) {
154
+ continue;
155
+ }
156
+ const key = entry[0];
157
+ const child = entry[1];
158
+
159
+ if (isSensitiveKey(key, options.sensitiveKeyMatcher)) {
160
+ out[key] = "[MASKED]";
161
+ continue;
162
+ }
163
+
164
+ out[key] = sanitizeAny(child, options, state, depth + 1, visited);
165
+ }
166
+
167
+ return out;
168
+ }
169
+
170
+ export function sanitizeLogObject(obj, options) {
171
+ const visited = new WeakSet();
172
+ const state = { nodes: 0 };
173
+
174
+ const out = {};
175
+ for (const [key, val] of Object.entries(obj)) {
176
+ if (isSensitiveKey(key, options.sensitiveKeyMatcher)) {
177
+ out[key] = "[MASKED]";
178
+ continue;
179
+ }
180
+
181
+ out[key] = sanitizeAny(val, options, state, 0, visited);
182
+ }
183
+
184
+ return out;
185
+ }