befly 0.1.26

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/utils/jwt.js ADDED
@@ -0,0 +1,154 @@
1
+ import { Env } from '../config/env.js';
2
+
3
+ /**
4
+ * JWT 工具类
5
+ */
6
+ export class Jwt {
7
+ static signer = null;
8
+ static verifier = null;
9
+ static initialized = false;
10
+
11
+ /**
12
+ * 初始化 JWT 工具
13
+ */
14
+ static async init() {
15
+ if (this.initialized) return;
16
+
17
+ const { createSigner, createVerifier } = await import('fast-jwt');
18
+
19
+ this.signer = createSigner({
20
+ key: Env.JWT_SECRET,
21
+ expiresIn: Env.JWT_EXPIRES_IN || '7d',
22
+ algorithm: Env.JWT_ALGORITHM || 'HS256'
23
+ });
24
+
25
+ this.verifier = createVerifier({
26
+ key: Env.JWT_SECRET,
27
+ algorithms: [Env.JWT_ALGORITHM || 'HS256']
28
+ });
29
+
30
+ this.initialized = true;
31
+ }
32
+
33
+ /**
34
+ * 签名 JWT token
35
+ * @param {object} payload - JWT 载荷
36
+ * @returns {Promise<string>} JWT token
37
+ */
38
+ static async sign(payload) {
39
+ await this.init();
40
+ return this.signer(payload);
41
+ }
42
+
43
+ /**
44
+ * 验证 JWT token
45
+ * @param {string} token - JWT token
46
+ * @returns {Promise<object>} 解码后的载荷
47
+ */
48
+ static async verify(token) {
49
+ await this.init();
50
+ return this.verifier(token);
51
+ }
52
+
53
+ /**
54
+ * 创建自定义签名器
55
+ * @param {object} options - 签名选项
56
+ * @returns {Promise<Function>} 签名器函数
57
+ */
58
+ static async createSigner(options = {}) {
59
+ const { createSigner } = await import('fast-jwt');
60
+ return createSigner({
61
+ key: options.key || Env.JWT_SECRET,
62
+ expiresIn: options.expiresIn || Env.JWT_EXPIRES_IN || '7d',
63
+ algorithm: options.algorithm || Env.JWT_ALGORITHM || 'HS256',
64
+ ...options
65
+ });
66
+ }
67
+
68
+ /**
69
+ * 创建自定义验证器
70
+ * @param {object} options - 验证选项
71
+ * @returns {Promise<Function>} 验证器函数
72
+ */
73
+ static async createVerifier(options = {}) {
74
+ const { createVerifier } = await import('fast-jwt');
75
+ return createVerifier({
76
+ key: options.key || Env.JWT_SECRET,
77
+ algorithms: options.algorithms || [Env.JWT_ALGORITHM || 'HS256'],
78
+ ...options
79
+ });
80
+ }
81
+
82
+ /**
83
+ * 解码 JWT token (不验证签名)
84
+ * @param {string} token - JWT token
85
+ * @returns {object} 解码后的内容
86
+ */
87
+ static decode(token) {
88
+ try {
89
+ const parts = token.split('.');
90
+ if (parts.length !== 3) {
91
+ throw new Error('Invalid JWT format');
92
+ }
93
+
94
+ const header = JSON.parse(atob(parts[0]));
95
+ const payload = JSON.parse(atob(parts[1]));
96
+
97
+ return { header, payload };
98
+ } catch (error) {
99
+ throw new Error('Failed to decode JWT: ' + error.message);
100
+ }
101
+ }
102
+
103
+ /**
104
+ * 检查 token 是否过期
105
+ * @param {string} token - JWT token
106
+ * @returns {boolean} 是否过期
107
+ */
108
+ static isExpired(token) {
109
+ try {
110
+ const { payload } = this.decode(token);
111
+ if (!payload.exp) return false;
112
+ return Date.now() >= payload.exp * 1000;
113
+ } catch {
114
+ return true;
115
+ }
116
+ }
117
+
118
+ /**
119
+ * 获取 token 剩余有效时间 (秒)
120
+ * @param {string} token - JWT token
121
+ * @returns {number} 剩余秒数,-1 表示已过期或无过期时间
122
+ */
123
+ static getTimeToExpiry(token) {
124
+ try {
125
+ const { payload } = this.decode(token);
126
+ if (!payload.exp) return -1;
127
+ const remaining = payload.exp - Math.floor(Date.now() / 1000);
128
+ return remaining > 0 ? remaining : -1;
129
+ } catch {
130
+ return -1;
131
+ }
132
+ }
133
+
134
+ /**
135
+ * 刷新 token (重新签名相同载荷)
136
+ * @param {string} token - 旧的 JWT token
137
+ * @returns {Promise<string>} 新的 JWT token
138
+ */
139
+ static async refresh(token) {
140
+ const { payload } = this.decode(token);
141
+ // 移除时间相关的声明,让新 token 重新生成
142
+ delete payload.iat;
143
+ delete payload.exp;
144
+ delete payload.nbf;
145
+ return this.sign(payload);
146
+ }
147
+ }
148
+
149
+ // 使用示例:
150
+ // const token = await JWT.sign({ userId: 123, role: 'user' });
151
+ // const payload = await JWT.verify(token);
152
+ // const isExpired = JWT.isExpired(token);
153
+ // const remaining = JWT.getTimeToExpiry(token);
154
+ // const newToken = await JWT.refresh(token);
@@ -0,0 +1,143 @@
1
+ import path from 'path';
2
+ import { appendFile, stat } from 'node:fs/promises';
3
+ import { formatDate } from './util.js';
4
+ import { Env } from '../config/env.js';
5
+
6
+ export class Logger {
7
+ // 静态属性
8
+ static level = Env.LOG_LEVEL || 'info';
9
+ static levels = {
10
+ error: 0,
11
+ warn: 1,
12
+ info: 2,
13
+ debug: 3
14
+ };
15
+ static logDir = Env.LOG_DIR || 'logs';
16
+ static maxFileSize = Env.LOG_MAX_SIZE || 50 * 1024 * 1024; // 50MB
17
+ static currentFiles = new Map(); // key: prefix, value: filepath
18
+
19
+ static formatMessage(level, message) {
20
+ const timestamp = formatDate();
21
+ const levelStr = level.toUpperCase().padStart(5);
22
+
23
+ let msg = `[${timestamp}] ${levelStr} - `;
24
+
25
+ if (Object.keys(message).length > 0) {
26
+ msg += `${JSON.stringify(message).replace(/\s+/g, ' ').replace(/\\"/g, '"').replace(/\\n/g, ' ')}`;
27
+ }
28
+
29
+ return msg;
30
+ }
31
+
32
+ static async log(level, message) {
33
+ // 内联 shouldLog 逻辑,检查日志级别
34
+ if (this.levels[level] > this.levels[this.level]) return;
35
+
36
+ const formattedMessage = this.formatMessage(level, message);
37
+
38
+ // 控制台输出
39
+ if (Env.LOG_TO_CONSOLE === 1) {
40
+ console.log(formattedMessage);
41
+ }
42
+
43
+ await this.writeToFile(formattedMessage, level);
44
+ }
45
+
46
+ static async writeToFile(message, level = 'info') {
47
+ try {
48
+ let prefix;
49
+
50
+ // debug 日志使用单独的文件名
51
+ if (level === 'debug') {
52
+ prefix = 'debug';
53
+ } else {
54
+ prefix = new Date().toISOString().split('T')[0];
55
+ }
56
+
57
+ // 检查缓存的当前文件是否仍然可用
58
+ let currentLogFile = this.currentFiles.get(prefix);
59
+
60
+ if (currentLogFile) {
61
+ try {
62
+ const stats = await stat(currentLogFile);
63
+ // 如果文件超过最大大小,清除缓存
64
+ if (stats.size >= this.maxFileSize) {
65
+ this.currentFiles.delete(prefix);
66
+ currentLogFile = null;
67
+ }
68
+ } catch (error) {
69
+ // 文件不存在或无法访问,清除缓存
70
+ this.currentFiles.delete(prefix);
71
+ currentLogFile = null;
72
+ }
73
+ }
74
+
75
+ // 如果没有缓存的文件或文件已满,查找合适的文件
76
+ if (!currentLogFile) {
77
+ currentLogFile = await this.findAvailableLogFile(prefix);
78
+ this.currentFiles.set(prefix, currentLogFile);
79
+ }
80
+
81
+ // 使用 Node.js 的 appendFile 进行文件追加
82
+ await appendFile(currentLogFile, message + '\n', 'utf8');
83
+ } catch (error) {
84
+ console.error('写入日志文件失败:', error.message);
85
+ }
86
+ }
87
+
88
+ static async findAvailableLogFile(prefix) {
89
+ const glob = new Bun.Glob(`${prefix}.*.log`);
90
+ const files = await Array.fromAsync(glob.scan(this.logDir));
91
+
92
+ // 按文件名排序
93
+ files.sort((a, b) => {
94
+ const aNum = parseInt(a.match(/\.(\d+)\.log$/)?.[1] || '0');
95
+ const bNum = parseInt(b.match(/\.(\d+)\.log$/)?.[1] || '0');
96
+ return aNum - bNum;
97
+ });
98
+
99
+ // 从最后一个文件开始检查
100
+ for (let i = files.length - 1; i >= 0; i--) {
101
+ const filePath = path.join(this.logDir, files[i]);
102
+ try {
103
+ const stats = await stat(filePath);
104
+ if (stats.size < this.maxFileSize) {
105
+ return filePath;
106
+ }
107
+ } catch (error) {
108
+ // 文件不存在或无法访问,跳过
109
+ continue;
110
+ }
111
+ }
112
+
113
+ // 所有文件都已满或没有文件,创建新文件
114
+ const nextIndex = files.length > 0 ? Math.max(...files.map((f) => parseInt(f.match(/\.(\d+)\.log$/)?.[1] || '0'))) + 1 : 0;
115
+
116
+ return path.join(this.logDir, `${prefix}.${nextIndex}.log`);
117
+ }
118
+
119
+ // 静态便捷方法
120
+ static async error(message) {
121
+ await this.log('error', message);
122
+ }
123
+
124
+ static async warn(message) {
125
+ await this.log('warn', message);
126
+ }
127
+
128
+ static async info(message) {
129
+ await this.log('info', message);
130
+ }
131
+
132
+ static async debug(message) {
133
+ // debug 级别必须记录,忽略级别检查
134
+ const formattedMessage = this.formatMessage('debug', message);
135
+
136
+ // 控制台输出
137
+ if (Env.LOG_TO_CONSOLE === 1) {
138
+ console.log(formattedMessage);
139
+ }
140
+
141
+ await this.writeToFile(formattedMessage, 'debug');
142
+ }
143
+ }
package/utils/util.js ADDED
@@ -0,0 +1,171 @@
1
+ import { fileURLToPath } from 'node:url';
2
+ import path from 'node:path';
3
+
4
+ export const sortPlugins = (plugins) => {
5
+ const result = [];
6
+ const visited = new Set();
7
+ const visiting = new Set();
8
+ const pluginMap = Object.fromEntries(plugins.map((p) => [p.pluginName, p]));
9
+ let isPass = true;
10
+ const visit = (name) => {
11
+ if (visited.has(name)) return;
12
+ if (visiting.has(name)) {
13
+ isPass = false;
14
+ return;
15
+ }
16
+
17
+ const plugin = pluginMap[name];
18
+ if (!plugin) return; // 依赖不存在时跳过
19
+
20
+ visiting.add(name);
21
+ (plugin.after || []).forEach(visit);
22
+ visiting.delete(name);
23
+ visited.add(name);
24
+ result.push(plugin);
25
+ };
26
+
27
+ plugins.forEach((p) => visit(p.pluginName));
28
+ return isPass ? result : false;
29
+ };
30
+
31
+ // 规则分割
32
+ export const ruleSplit = (rule) => {
33
+ const allParts = rule.split(',');
34
+
35
+ // 如果部分数量小于等于5,直接返回
36
+ if (allParts.length <= 5) {
37
+ return allParts;
38
+ }
39
+
40
+ // 只取前4个部分,剩余的都合并为第5个部分
41
+ return [allParts[0], allParts[1], allParts[2], allParts[3], allParts.slice(4).join(',')];
42
+ };
43
+
44
+ export const formatDate = (date = new Date(), format = 'YYYY-MM-DD HH:mm:ss') => {
45
+ const d = new Date(date);
46
+ const year = d.getFullYear();
47
+ const month = String(d.getMonth() + 1).padStart(2, '0');
48
+ const day = String(d.getDate()).padStart(2, '0');
49
+ const hour = String(d.getHours()).padStart(2, '0');
50
+ const minute = String(d.getMinutes()).padStart(2, '0');
51
+ const second = String(d.getSeconds()).padStart(2, '0');
52
+
53
+ return format.replace('YYYY', year).replace('MM', month).replace('DD', day).replace('HH', hour).replace('mm', minute).replace('ss', second);
54
+ };
55
+
56
+ // 类型判断
57
+ export const isType = (value, type) => {
58
+ const actualType = Object.prototype.toString.call(value).slice(8, -1).toLowerCase();
59
+ const expectedType = type.toLowerCase();
60
+
61
+ // 特殊类型处理
62
+ switch (expectedType) {
63
+ case 'null':
64
+ return value === null;
65
+ case 'undefined':
66
+ return value === undefined;
67
+ case 'nan':
68
+ return Number.isNaN(value);
69
+ case 'empty':
70
+ return value === '' || value === null || value === undefined;
71
+ case 'integer':
72
+ return Number.isInteger(value);
73
+ case 'float':
74
+ return typeof value === 'number' && !Number.isInteger(value) && !Number.isNaN(value);
75
+ case 'positive':
76
+ return typeof value === 'number' && value > 0;
77
+ case 'negative':
78
+ return typeof value === 'number' && value < 0;
79
+ case 'zero':
80
+ return value === 0;
81
+ case 'truthy':
82
+ return !!value;
83
+ case 'falsy':
84
+ return !value;
85
+ case 'primitive':
86
+ return value !== Object(value);
87
+ case 'reference':
88
+ return value === Object(value);
89
+ default:
90
+ return actualType === expectedType;
91
+ }
92
+ };
93
+
94
+ export const pickFields = (obj, keys) => {
95
+ if (!obj || typeof obj !== 'object') {
96
+ return {};
97
+ }
98
+
99
+ const result = {};
100
+
101
+ for (const key of keys) {
102
+ if (key in obj) {
103
+ result[key] = obj[key];
104
+ }
105
+ }
106
+
107
+ return result;
108
+ };
109
+
110
+ export const omitFields = (obj, keys) => {
111
+ if (!obj || typeof obj !== 'object' || !Array.isArray(keys)) {
112
+ return {};
113
+ }
114
+
115
+ const result = {};
116
+
117
+ for (const key in obj) {
118
+ if (obj.hasOwnProperty(key) && !keys.includes(key)) {
119
+ result[key] = obj[key];
120
+ }
121
+ }
122
+
123
+ return result;
124
+ };
125
+
126
+ export const isEmptyObject = (obj) => {
127
+ // 首先检查是否为对象
128
+ if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
129
+ return false;
130
+ }
131
+
132
+ // 检查是否为空对象
133
+ return Object.keys(obj).length === 0;
134
+ };
135
+
136
+ export const isEmptyArray = (arr) => {
137
+ // 首先检查是否为数组
138
+ if (!Array.isArray(arr)) {
139
+ return false;
140
+ }
141
+
142
+ // 检查是否为空数组
143
+ return arr.length === 0;
144
+ };
145
+
146
+ // 返回结果
147
+ export const RYes = (msg = '', data = {}, other = {}) => {
148
+ return {
149
+ ...other,
150
+ code: 0,
151
+ msg: msg,
152
+ data: data
153
+ };
154
+ };
155
+
156
+ export const RNo = (msg = '', data = {}, other = {}) => {
157
+ return {
158
+ ...other,
159
+ code: 1,
160
+ msg: msg,
161
+ data: data
162
+ };
163
+ };
164
+
165
+ export const filename2 = (importMetaUrl) => {
166
+ return fileURLToPath(importMetaUrl);
167
+ };
168
+
169
+ export const dirname2 = (importMetaUrl) => {
170
+ return path.dirname(fileURLToPath(importMetaUrl));
171
+ };
@@ -0,0 +1,244 @@
1
+ import { ruleSplit, isType } from './util.js';
2
+
3
+ /**
4
+ * 验证器类
5
+ */
6
+ export class Validator {
7
+ /**
8
+ * 验证数据
9
+ * @param {Object} data - 要验证的数据对象
10
+ * @param {Object} rules - 验证规则对象
11
+ * @param {Array} required - 必传字段数组
12
+ * @returns {Object} { code: 0|1, fields: {} }
13
+ */
14
+ validate(data, rules, required = []) {
15
+ const result = {
16
+ code: 0,
17
+ fields: {}
18
+ };
19
+
20
+ // 参数检查
21
+ if (!this.checkParams(data, rules, required, result)) {
22
+ return result;
23
+ }
24
+
25
+ // 检查必传字段
26
+ this.checkRequiredFields(data, rules, required, result);
27
+
28
+ // 验证所有在规则中定义的字段
29
+ this.validateFields(data, rules, required, result);
30
+
31
+ return result;
32
+ }
33
+
34
+ /**
35
+ * 检查参数有效性
36
+ */
37
+ checkParams(data, rules, required, result) {
38
+ if (!data || typeof data !== 'object') {
39
+ result.code = 1;
40
+ result.fields.error = '数据必须是对象格式';
41
+ return false;
42
+ }
43
+
44
+ if (!rules || typeof rules !== 'object') {
45
+ result.code = 1;
46
+ result.fields.error = '验证规则必须是对象格式';
47
+ return false;
48
+ }
49
+
50
+ if (!Array.isArray(required)) {
51
+ result.code = 1;
52
+ result.fields.error = '必传字段必须是数组格式';
53
+ return false;
54
+ }
55
+
56
+ return true;
57
+ }
58
+
59
+ /**
60
+ * 检查必传字段
61
+ */
62
+ checkRequiredFields(data, rules, required, result) {
63
+ for (const fieldName of required) {
64
+ if (!(fieldName in data) || data[fieldName] === undefined || data[fieldName] === null || data[fieldName] === '') {
65
+ result.code = 1;
66
+ const ruleParts = rules[fieldName]?.split(',') || [];
67
+ const fieldLabel = ruleParts[0] || fieldName;
68
+ result.fields[fieldName] = `${fieldLabel}(${fieldName})为必填项`;
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * 验证所有字段
75
+ */
76
+ validateFields(data, rules, required, result) {
77
+ for (const [fieldName, rule] of Object.entries(rules)) {
78
+ // 如果字段不存在且不是必传字段,跳过验证
79
+ if (!(fieldName in data) && !required.includes(fieldName)) {
80
+ continue;
81
+ }
82
+
83
+ // 如果必传验证已经失败,跳过后续验证
84
+ if (result.fields[fieldName]) {
85
+ continue;
86
+ }
87
+
88
+ const value = data[fieldName];
89
+ const error = this.validateFieldValue(value, rule, fieldName);
90
+
91
+ if (error) {
92
+ result.code = 1;
93
+ result.fields[fieldName] = error;
94
+ }
95
+ }
96
+ }
97
+
98
+ /**
99
+ * 验证单个字段的值
100
+ */
101
+ validateFieldValue(value, rule, fieldName) {
102
+ const [name, type, minStr, maxStr, specStr] = ruleSplit(rule);
103
+ const min = minStr === 'null' ? null : parseInt(minStr) || 0;
104
+ const max = maxStr === 'null' ? null : parseInt(maxStr) || 0;
105
+ const spec = specStr === 'null' ? null : specStr.trim();
106
+
107
+ switch (type.toLowerCase()) {
108
+ case 'number':
109
+ return this.validateNumber(value, name, min, max, spec, fieldName);
110
+ case 'string':
111
+ return this.validateString(value, name, min, max, spec, fieldName);
112
+ case 'array':
113
+ return this.validateArray(value, name, min, max, spec, fieldName);
114
+ default:
115
+ return `字段 ${fieldName} 的类型 ${type} 不支持`;
116
+ }
117
+ }
118
+
119
+ /**
120
+ * 验证数字类型
121
+ */
122
+ validateNumber(value, name, min, max, spec, fieldName) {
123
+ try {
124
+ if (isType(value, 'number') === false) {
125
+ return `${name}(${fieldName})必须是数字`;
126
+ }
127
+
128
+ if (min !== null && value < min) {
129
+ return `${name}(${fieldName})不能小于${min}`;
130
+ }
131
+
132
+ if (min !== null && max > 0 && value > max) {
133
+ return `${name}(${fieldName})不能大于${max}`;
134
+ }
135
+
136
+ if (spec && spec.trim() !== '') {
137
+ return this.validateNumberExpression(value, name, spec, fieldName);
138
+ }
139
+
140
+ return null;
141
+ } catch (error) {
142
+ return `${name}(${fieldName})的计算规则格式错误: ${error.message}`;
143
+ }
144
+ }
145
+
146
+ /**
147
+ * 验证数字表达式
148
+ */
149
+ validateNumberExpression(value, name, spec, fieldName) {
150
+ const parts = spec.split('=');
151
+ if (parts.length !== 2) {
152
+ return `${name}(${fieldName})的计算规则必须包含等号`;
153
+ }
154
+
155
+ const leftExpression = parts[0].trim();
156
+ const rightValue = parseFloat(parts[1].trim());
157
+
158
+ if (isNaN(rightValue)) {
159
+ return `${name}(${fieldName})的计算规则右边必须是数字`;
160
+ }
161
+
162
+ const safePattern = /^[x\d\+\-\*\/\(\)\.\s]+$/;
163
+ if (!safePattern.test(leftExpression)) {
164
+ return `${name}(${fieldName})的表达式包含不安全的字符`;
165
+ }
166
+
167
+ let processedExpression = leftExpression.replace(/x/g, value.toString());
168
+ const leftResult = new Function('return ' + processedExpression)();
169
+
170
+ if (typeof leftResult !== 'number' || !isFinite(leftResult)) {
171
+ return `${name}(${fieldName})的表达式计算结果不是有效数字`;
172
+ }
173
+
174
+ if (Math.abs(leftResult - rightValue) > Number.EPSILON) {
175
+ return `${name}(${fieldName})不满足计算条件 ${spec}`;
176
+ }
177
+
178
+ return null;
179
+ }
180
+
181
+ /**
182
+ * 验证字符串类型
183
+ */
184
+ validateString(value, name, min, max, spec, fieldName) {
185
+ try {
186
+ if (isType(value, 'string') === false) {
187
+ return `${name}(${fieldName})必须是字符串`;
188
+ }
189
+
190
+ if (min !== null && value.length < min) {
191
+ return `${name}(${fieldName})长度不能少于${min}个字符`;
192
+ }
193
+
194
+ if (max !== null && max > 0 && value.length > max) {
195
+ return `${name}(${fieldName})长度不能超过${max}个字符`;
196
+ }
197
+
198
+ if (spec && spec.trim() !== '') {
199
+ const regExp = new RegExp(spec);
200
+ if (!regExp.test(value)) {
201
+ return `${name}(${fieldName})格式不正确`;
202
+ }
203
+ }
204
+
205
+ return null;
206
+ } catch (error) {
207
+ return `${name}(${fieldName})的正则表达式格式错误`;
208
+ }
209
+ }
210
+
211
+ /**
212
+ * 验证数组类型
213
+ */
214
+ validateArray(value, name, min, max, spec, fieldName) {
215
+ try {
216
+ if (!Array.isArray(value)) {
217
+ return `${name}(${fieldName})必须是数组`;
218
+ }
219
+
220
+ if (min !== null && value.length < min) {
221
+ return `${name}(${fieldName})至少需要${min}个元素`;
222
+ }
223
+
224
+ if (max !== null && max > 0 && value.length > max) {
225
+ return `${name}(${fieldName})最多只能有${max}个元素`;
226
+ }
227
+
228
+ if (spec && spec.trim() !== '') {
229
+ const regExp = new RegExp(spec);
230
+ for (const item of value) {
231
+ if (!regExp.test(String(item))) {
232
+ return `${name}(${fieldName})中的元素"${item}"格式不正确`;
233
+ }
234
+ }
235
+ }
236
+
237
+ return null;
238
+ } catch (error) {
239
+ return `${name}(${fieldName})的正则表达式格式错误: ${error.message}`;
240
+ }
241
+ }
242
+ }
243
+
244
+ export const validator = new Validator();