befly 3.71.0 → 3.73.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/checks/api.js +50 -107
- package/checks/config.js +5 -133
- package/checks/hook.js +28 -31
- package/checks/menu.js +36 -50
- package/checks/plugin.js +28 -31
- package/checks/table.js +25 -109
- package/configs/beflyConfig.json +1 -3
- package/hooks/rateLimit.js +25 -186
- package/hooks/validator.js +2 -2
- package/index.js +1 -1
- package/lib/validator.js +208 -304
- package/package.json +3 -3
- package/plugins/cron.js +2 -2
- package/schemas/config.js +67 -0
- package/utils/cors.js +2 -2
- package/utils/formatValidationIssues.js +28 -0
- package/utils/is.js +4 -3
- package/utils/formatZodIssues.js +0 -98
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import { RUN_MODE_VALUES } from "../configs/constConfig.js";
|
|
2
|
+
|
|
3
|
+
const noTrimString = { type: "string", noTrim: true };
|
|
4
|
+
const boolInt = { type: "boolInt" };
|
|
5
|
+
const port = { type: "number", integer: true, min: 1, max: 65535 };
|
|
6
|
+
|
|
7
|
+
export const configSchema = {
|
|
8
|
+
root: {
|
|
9
|
+
runMode: { type: "values", values: RUN_MODE_VALUES },
|
|
10
|
+
appName: noTrimString,
|
|
11
|
+
appPort: port,
|
|
12
|
+
appHost: noTrimString,
|
|
13
|
+
apiHost: noTrimString,
|
|
14
|
+
devEmail: { type: "string", noTrim: true, check: "emailOrEmpty" },
|
|
15
|
+
devPassword: { type: "string", min: 6 },
|
|
16
|
+
bodyLimit: { type: "number", integer: true, min: 1 },
|
|
17
|
+
tz: { type: "string", check: "timeZone" },
|
|
18
|
+
excludeApisLog: { type: "stringArray", noTrim: true }
|
|
19
|
+
},
|
|
20
|
+
groups: {
|
|
21
|
+
upload: {
|
|
22
|
+
maxSize: { type: "number", integer: true, min: 1 },
|
|
23
|
+
publicDir: { ...noTrimString, min: 1 },
|
|
24
|
+
publicHost: noTrimString,
|
|
25
|
+
allowedExtensions: { ...noTrimString, pattern: /^\.[a-z0-9]+(?:,\.[a-z0-9]+)*$/ },
|
|
26
|
+
allowedMimeTypes: { ...noTrimString, pattern: /^[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*(?:,[a-z0-9][a-z0-9.+-]*\/[a-z0-9][a-z0-9.+-]*)*$/ },
|
|
27
|
+
forceDownloadExtensions: { ...noTrimString, pattern: /^(|\.[a-z0-9]+(?:,\.[a-z0-9]+)*)$/ }
|
|
28
|
+
},
|
|
29
|
+
logger: { debug: boolInt, excludeFields: { type: "stringArray", noTrim: true } },
|
|
30
|
+
mysql: {
|
|
31
|
+
hostname: noTrimString,
|
|
32
|
+
port: port,
|
|
33
|
+
username: noTrimString,
|
|
34
|
+
password: noTrimString,
|
|
35
|
+
database: noTrimString,
|
|
36
|
+
max: { type: "number", min: 1 },
|
|
37
|
+
beflyMode: { type: "values", values: ["manual", "auto"], optional: true },
|
|
38
|
+
idleTimeout: { type: "number", min: 1, optional: true },
|
|
39
|
+
maxLifetime: { type: "number", min: 0, optional: true },
|
|
40
|
+
connectionTimeout: { type: "number", min: 1, optional: true }
|
|
41
|
+
},
|
|
42
|
+
redis: {
|
|
43
|
+
hostname: noTrimString,
|
|
44
|
+
port: port,
|
|
45
|
+
username: noTrimString,
|
|
46
|
+
password: noTrimString,
|
|
47
|
+
db: { type: "number", integer: true, min: 0, max: 16 },
|
|
48
|
+
prefix: { ...noTrimString, check: "redisPrefix" }
|
|
49
|
+
},
|
|
50
|
+
session: { expireDays: { type: "number", integer: true, min: 1 } },
|
|
51
|
+
email: { host: noTrimString, port: port, secure: boolInt, user: noTrimString, pass: noTrimString, label: { ...noTrimString, optional: true } },
|
|
52
|
+
cors: {
|
|
53
|
+
methods: noTrimString,
|
|
54
|
+
allowedHeaders: noTrimString,
|
|
55
|
+
exposedHeaders: noTrimString,
|
|
56
|
+
maxAge: { type: "number", integer: true, min: 0 },
|
|
57
|
+
credentials: { type: "values", values: ["true", "false", true, false] }
|
|
58
|
+
},
|
|
59
|
+
rateLimit: {
|
|
60
|
+
enable: boolInt,
|
|
61
|
+
defaultLimit: { type: "number", integer: true, min: 1 },
|
|
62
|
+
defaultWindow: { type: "number", integer: true, min: 1 },
|
|
63
|
+
key: noTrimString,
|
|
64
|
+
skipRoutes: { type: "stringArray", noTrim: true }
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
};
|
package/utils/cors.js
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* @returns CORS 响应头对象
|
|
6
6
|
*/
|
|
7
7
|
export function setCorsOptions(req, config) {
|
|
8
|
-
const origin =
|
|
8
|
+
const origin = req.headers.get("origin");
|
|
9
9
|
const allowCredentials = config.credentials === true || config.credentials === "true" ? "true" : "false";
|
|
10
10
|
const requestedHeaders = req.headers.get("Access-Control-Request-Headers") || "";
|
|
11
11
|
|
|
@@ -42,7 +42,7 @@ export function setCorsOptions(req, config) {
|
|
|
42
42
|
const allowHeaders = allowedHeaderItems.join(", ");
|
|
43
43
|
|
|
44
44
|
return {
|
|
45
|
-
"Access-Control-Allow-Origin": origin
|
|
45
|
+
...(origin ? { "Access-Control-Allow-Origin": origin, Vary: "Origin" } : {}),
|
|
46
46
|
"Access-Control-Allow-Methods": config.methods,
|
|
47
47
|
"Access-Control-Allow-Headers": allowHeaders,
|
|
48
48
|
"Access-Control-Expose-Headers": config.exposedHeaders,
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
function pickFileLabel(item, itemLabel, index) {
|
|
2
|
+
if (item) {
|
|
3
|
+
if (item.filePath) return item.filePath;
|
|
4
|
+
if (item.relativePath) return item.relativePath;
|
|
5
|
+
if (item.fileName) return item.fileName;
|
|
6
|
+
if (item.name) return item.name;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
return typeof index === "number" ? `${itemLabel}[${index}]` : itemLabel;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function formatValidationIssues(issues, options = {}) {
|
|
13
|
+
const items = Array.isArray(options.items) ? options.items : null;
|
|
14
|
+
const item = options.item || null;
|
|
15
|
+
const itemLabel = options.itemLabel || "item";
|
|
16
|
+
|
|
17
|
+
return issues.map((issue) => {
|
|
18
|
+
const path = Array.isArray(issue.path) ? issue.path : [];
|
|
19
|
+
const hasIndex = items && typeof path[0] === "number";
|
|
20
|
+
const index = hasIndex ? path[0] : null;
|
|
21
|
+
const fieldPath = (hasIndex ? path.slice(1) : path).join(".") || "(root)";
|
|
22
|
+
|
|
23
|
+
return {
|
|
24
|
+
file: pickFileLabel(hasIndex ? items[index] : item, itemLabel, index),
|
|
25
|
+
expected: `字段 ${fieldPath} ${issue.message}`
|
|
26
|
+
};
|
|
27
|
+
});
|
|
28
|
+
}
|
package/utils/is.js
CHANGED
|
@@ -113,7 +113,7 @@ export function isBoolean(value) {
|
|
|
113
113
|
* 判断值是否为 function。
|
|
114
114
|
*/
|
|
115
115
|
export function isFunction(value) {
|
|
116
|
-
return
|
|
116
|
+
return typeof value === "function";
|
|
117
117
|
}
|
|
118
118
|
|
|
119
119
|
/**
|
|
@@ -349,8 +349,9 @@ export function isEmpty(value) {
|
|
|
349
349
|
* 判断当前进程是否为主进程。
|
|
350
350
|
* PM2 cluster 的实例 0 是主进程;未由 PM2 启动时视为主进程。
|
|
351
351
|
*/
|
|
352
|
-
export function isPrimaryProcess(
|
|
353
|
-
|
|
352
|
+
export function isPrimaryProcess() {
|
|
353
|
+
const instance = Bun.env.NODE_APP_INSTANCE;
|
|
354
|
+
return instance === "0" || instance === 0 || instance === undefined;
|
|
354
355
|
}
|
|
355
356
|
|
|
356
357
|
export const isDirentDirectory = (parentDir, entry) => {
|
package/utils/formatZodIssues.js
DELETED
|
@@ -1,98 +0,0 @@
|
|
|
1
|
-
function formatValue(value) {
|
|
2
|
-
if (value === undefined) return "undefined";
|
|
3
|
-
if (value === null) return "null";
|
|
4
|
-
if (typeof value === "string") return value;
|
|
5
|
-
if (typeof value === "number" || typeof value === "boolean") return value;
|
|
6
|
-
if (typeof value === "function") return "[Function]";
|
|
7
|
-
if (Array.isArray(value)) return value.map((item) => formatValue(item)).join(", ");
|
|
8
|
-
|
|
9
|
-
try {
|
|
10
|
-
return JSON.stringify(value);
|
|
11
|
-
} catch {
|
|
12
|
-
return String(value);
|
|
13
|
-
}
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
function pickFileLabel(item, itemLabel, index) {
|
|
17
|
-
if (item) {
|
|
18
|
-
if (item.filePath) return item.filePath;
|
|
19
|
-
if (item.relativePath) return item.relativePath;
|
|
20
|
-
if (item.fileName) return item.fileName;
|
|
21
|
-
if (item.name) return item.name;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
if (typeof index === "number") return `${itemLabel}[${index}]`;
|
|
25
|
-
|
|
26
|
-
return itemLabel;
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
function buildExpectedMessage(issue, fieldPath) {
|
|
30
|
-
if (issue.code === "invalid_enum_value" && Array.isArray(issue.options)) {
|
|
31
|
-
return `字段 ${fieldPath} 仅允许 ${issue.options.join("|")}`;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
if (issue.code === "invalid_literal") {
|
|
35
|
-
return `字段 ${fieldPath} 仅允许 ${formatValue(issue.expected)}`;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
if (issue.code === "invalid_type") {
|
|
39
|
-
return `字段 ${fieldPath} 必须是 ${issue.expected}`;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
if (issue.code === "too_small") {
|
|
43
|
-
if (typeof issue.minimum === "number") {
|
|
44
|
-
const sign = issue.inclusive ? ">=" : ">";
|
|
45
|
-
return `字段 ${fieldPath} ${sign}${issue.minimum}`;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (issue.code === "too_big") {
|
|
50
|
-
if (typeof issue.maximum === "number") {
|
|
51
|
-
const sign = issue.inclusive ? "<=" : "<";
|
|
52
|
-
return `字段 ${fieldPath} ${sign}${issue.maximum}`;
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
if (issue.message) {
|
|
57
|
-
return `字段 ${fieldPath} ${issue.message}`;
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
return `字段 ${fieldPath} 无效`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function formatZodIssues(issues, options = {}) {
|
|
64
|
-
const result = [];
|
|
65
|
-
const items = Array.isArray(options.items) ? options.items : null;
|
|
66
|
-
const item = options.item || null;
|
|
67
|
-
const itemLabel = options.itemLabel || "item";
|
|
68
|
-
|
|
69
|
-
for (const issue of issues) {
|
|
70
|
-
const issuePath = Array.isArray(issue.path) ? issue.path : [];
|
|
71
|
-
const hasIndex = items && typeof issuePath[0] === "number";
|
|
72
|
-
const index = hasIndex ? issuePath[0] : null;
|
|
73
|
-
const path = hasIndex ? issuePath.slice(1) : issuePath;
|
|
74
|
-
|
|
75
|
-
const targetItem = hasIndex ? items[index] : item;
|
|
76
|
-
const fileLabel = pickFileLabel(targetItem, itemLabel, index);
|
|
77
|
-
const fieldPath = path.length > 0 ? path.join(".") : "(root)";
|
|
78
|
-
|
|
79
|
-
const expectedMessage = buildExpectedMessage(issue, fieldPath);
|
|
80
|
-
|
|
81
|
-
if (issue.code === "unrecognized_keys" && Array.isArray(issue.keys)) {
|
|
82
|
-
for (const key of issue.keys) {
|
|
83
|
-
result.push({
|
|
84
|
-
file: fileLabel,
|
|
85
|
-
expected: `字段 ${key} 禁止出现`
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
continue;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
result.push({
|
|
92
|
-
file: fileLabel,
|
|
93
|
-
expected: expectedMessage
|
|
94
|
-
});
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
return result;
|
|
98
|
-
}
|