befly 2.3.3 → 3.0.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.
Files changed (93) hide show
  1. package/apis/health/info.ts +64 -0
  2. package/apis/tool/tokenCheck.ts +51 -0
  3. package/bin/befly.ts +202 -0
  4. package/checks/conflict.ts +408 -0
  5. package/checks/{table.js → table.ts} +139 -61
  6. package/config/env.ts +218 -0
  7. package/config/reserved.ts +96 -0
  8. package/main.ts +101 -0
  9. package/package.json +44 -8
  10. package/plugins/{db.js → db.ts} +24 -11
  11. package/plugins/logger.ts +28 -0
  12. package/plugins/redis.ts +51 -0
  13. package/plugins/tool.ts +34 -0
  14. package/scripts/syncDb/apply.ts +171 -0
  15. package/scripts/syncDb/constants.ts +70 -0
  16. package/scripts/syncDb/ddl.ts +182 -0
  17. package/scripts/syncDb/helpers.ts +172 -0
  18. package/scripts/syncDb/index.ts +215 -0
  19. package/scripts/syncDb/schema.ts +199 -0
  20. package/scripts/syncDb/sqlite.ts +50 -0
  21. package/scripts/syncDb/state.ts +104 -0
  22. package/scripts/syncDb/table.ts +204 -0
  23. package/scripts/syncDb/tableCreate.ts +142 -0
  24. package/scripts/syncDb/tests/constants.test.ts +104 -0
  25. package/scripts/syncDb/tests/ddl.test.ts +134 -0
  26. package/scripts/syncDb/tests/helpers.test.ts +70 -0
  27. package/scripts/syncDb/types.ts +92 -0
  28. package/scripts/syncDb/version.ts +73 -0
  29. package/scripts/syncDb.ts +9 -0
  30. package/scripts/{syncDev.js → syncDev.ts} +41 -25
  31. package/system.ts +149 -0
  32. package/tables/_common.json +21 -0
  33. package/tables/admin.json +10 -0
  34. package/tsconfig.json +58 -0
  35. package/types/api.d.ts +246 -0
  36. package/types/befly.d.ts +234 -0
  37. package/types/common.d.ts +215 -0
  38. package/types/context.ts +167 -0
  39. package/types/crypto.d.ts +23 -0
  40. package/types/database.d.ts +278 -0
  41. package/types/index.d.ts +16 -0
  42. package/types/index.ts +459 -0
  43. package/types/jwt.d.ts +99 -0
  44. package/types/logger.d.ts +43 -0
  45. package/types/plugin.d.ts +109 -0
  46. package/types/redis.d.ts +44 -0
  47. package/types/tool.d.ts +67 -0
  48. package/types/validator.d.ts +45 -0
  49. package/utils/addonHelper.ts +60 -0
  50. package/utils/api.ts +23 -0
  51. package/utils/{colors.js → colors.ts} +79 -21
  52. package/utils/crypto.ts +308 -0
  53. package/utils/datetime.ts +51 -0
  54. package/utils/dbHelper.ts +142 -0
  55. package/utils/errorHandler.ts +68 -0
  56. package/utils/index.ts +46 -0
  57. package/utils/jwt.ts +493 -0
  58. package/utils/logger.ts +284 -0
  59. package/utils/objectHelper.ts +68 -0
  60. package/utils/pluginHelper.ts +62 -0
  61. package/utils/redisHelper.ts +338 -0
  62. package/utils/response.ts +38 -0
  63. package/utils/{sqlBuilder.js → sqlBuilder.ts} +233 -97
  64. package/utils/sqlHelper.ts +447 -0
  65. package/utils/tableHelper.ts +167 -0
  66. package/utils/tool.ts +230 -0
  67. package/utils/typeHelper.ts +101 -0
  68. package/utils/validate.ts +451 -0
  69. package/utils/{xml.js → xml.ts} +100 -74
  70. package/.npmrc +0 -3
  71. package/.prettierignore +0 -2
  72. package/.prettierrc +0 -11
  73. package/apis/health/info.js +0 -49
  74. package/apis/tool/tokenCheck.js +0 -29
  75. package/bin/befly.js +0 -109
  76. package/config/env.js +0 -64
  77. package/main.js +0 -579
  78. package/plugins/logger.js +0 -14
  79. package/plugins/redis.js +0 -32
  80. package/plugins/tool.js +0 -8
  81. package/scripts/syncDb.js +0 -752
  82. package/system.js +0 -118
  83. package/tables/common.json +0 -16
  84. package/tables/tool.json +0 -6
  85. package/utils/api.js +0 -27
  86. package/utils/crypto.js +0 -260
  87. package/utils/index.js +0 -334
  88. package/utils/jwt.js +0 -387
  89. package/utils/logger.js +0 -143
  90. package/utils/redisHelper.js +0 -74
  91. package/utils/sqlManager.js +0 -471
  92. package/utils/tool.js +0 -31
  93. package/utils/validate.js +0 -226
package/utils/jwt.ts ADDED
@@ -0,0 +1,493 @@
1
+ /**
2
+ * JWT 工具类 - TypeScript 版本
3
+ * 提供JWT token的签名、验证和解码功能以及应用层的便捷接口
4
+ */
5
+
6
+ import { createHmac } from 'crypto';
7
+ import { Env } from '../config/env.js';
8
+ import type { JwtPayload, JwtSignOptions, JwtVerifyOptions, JwtAlgorithm, JwtHeader, JwtDecoded } from '../types/jwt';
9
+
10
+ /**
11
+ * JWT 工具类
12
+ */
13
+ export class Jwt {
14
+ /** 算法映射 */
15
+ private static readonly ALGORITHMS: Record<JwtAlgorithm, string> = {
16
+ HS256: 'sha256',
17
+ HS384: 'sha384',
18
+ HS512: 'sha512'
19
+ };
20
+
21
+ /**
22
+ * Base64 URL 编码
23
+ */
24
+ static base64UrlEncode(input: string | Buffer): string {
25
+ const base64 = Buffer.isBuffer(input) ? input.toString('base64') : Buffer.from(input, 'utf8').toString('base64');
26
+ return base64.replace(/\+/g, '-').replace(/\//g, '_').replace(/=/g, '');
27
+ }
28
+
29
+ /**
30
+ * Base64 URL 解码
31
+ */
32
+ static base64UrlDecode(str: string): string {
33
+ let padding = 4 - (str.length % 4);
34
+ if (padding !== 4) str += '='.repeat(padding);
35
+ str = str.replace(/-/g, '+').replace(/_/g, '/');
36
+ return Buffer.from(str, 'base64').toString('utf8');
37
+ }
38
+
39
+ /**
40
+ * 解析过期时间
41
+ * @param expiresIn - 过期时间(数字秒数或字符串如 "1h", "7d", "-1s")
42
+ * @returns 秒数(可以是负数)
43
+ */
44
+ static parseExpiration(expiresIn: string | number): number {
45
+ if (typeof expiresIn === 'number') return expiresIn;
46
+ if (typeof expiresIn !== 'string') throw new Error('过期时间格式无效');
47
+
48
+ const numericValue = parseInt(expiresIn);
49
+ if (!isNaN(numericValue) && numericValue.toString() === expiresIn) return numericValue;
50
+
51
+ // 支持负数时间(用于测试过期token)
52
+ const match = expiresIn.match(/^(-?\d+)(ms|[smhdwy])$/);
53
+ if (!match) throw new Error('过期时间格式无效');
54
+
55
+ const value = parseInt(match[1]); // 包含正负号
56
+ const unit = match[2];
57
+
58
+ if (unit === 'ms') return Math.floor(value / 1000);
59
+
60
+ const multipliers: Record<string, number> = {
61
+ s: 1,
62
+ m: 60,
63
+ h: 3600,
64
+ d: 86400,
65
+ w: 604800,
66
+ y: 31536000
67
+ };
68
+
69
+ return value * multipliers[unit];
70
+ }
71
+
72
+ /**
73
+ * 创建签名
74
+ */
75
+ private static createSignature(algorithm: JwtAlgorithm, secret: string, data: string): string {
76
+ const hashAlgorithm = this.ALGORITHMS[algorithm];
77
+ if (!hashAlgorithm) throw new Error(`不支持的算法: ${algorithm}`);
78
+
79
+ const hmac = createHmac(hashAlgorithm, secret);
80
+ hmac.update(data);
81
+ return this.base64UrlEncode(hmac.digest());
82
+ }
83
+
84
+ /**
85
+ * 常量时间比较(防止时序攻击)
86
+ */
87
+ private static constantTimeCompare(a: string, b: string): boolean {
88
+ if (a.length !== b.length) return false;
89
+ let result = 0;
90
+ for (let i = 0; i < a.length; i++) {
91
+ result |= a.charCodeAt(i) ^ b.charCodeAt(i);
92
+ }
93
+ return result === 0;
94
+ }
95
+
96
+ /**
97
+ * 签名 JWT token
98
+ * @param payload - JWT载荷数据
99
+ * @param options - 签名选项
100
+ * @returns JWT token字符串
101
+ */
102
+ static sign(payload: JwtPayload, options: JwtSignOptions): string;
103
+ static sign(payload: JwtPayload, secret: string, expiresIn: string | number): string;
104
+ static sign(payload: JwtPayload, optionsOrSecret: JwtSignOptions | string = {}, expiresIn?: string | number): string {
105
+ if (!payload || typeof payload !== 'object') {
106
+ throw new Error('载荷必须是非空对象');
107
+ }
108
+
109
+ // 支持两种调用方式
110
+ let options: JwtSignOptions;
111
+ if (typeof optionsOrSecret === 'string') {
112
+ // sign(payload, secret, expiresIn) 形式
113
+ options = {
114
+ secret: optionsOrSecret,
115
+ expiresIn: expiresIn
116
+ };
117
+ } else {
118
+ // sign(payload, options) 形式
119
+ options = optionsOrSecret;
120
+ }
121
+
122
+ const secret = options.secret || Env.JWT_SECRET;
123
+ const algorithm = (options.algorithm || Env.JWT_ALGORITHM || 'HS256') as JwtAlgorithm;
124
+
125
+ if (!secret) {
126
+ throw new Error('JWT密钥是必需的');
127
+ }
128
+
129
+ const now = Math.floor(Date.now() / 1000);
130
+
131
+ // 创建 header
132
+ const header = Jwt.base64UrlEncode(
133
+ JSON.stringify({
134
+ alg: algorithm,
135
+ typ: 'JWT'
136
+ })
137
+ );
138
+
139
+ // 创建 payload
140
+ const jwtPayload: JwtPayload = { ...payload, iat: now };
141
+
142
+ if (options.expiresIn || Env.JWT_EXPIRES_IN) {
143
+ const expSeconds = Jwt.parseExpiration(options.expiresIn || Env.JWT_EXPIRES_IN);
144
+ jwtPayload.exp = now + expSeconds;
145
+ }
146
+ if (options.issuer) jwtPayload.iss = options.issuer;
147
+ if (options.audience) jwtPayload.aud = options.audience;
148
+ if (options.subject) jwtPayload.sub = options.subject;
149
+ if (options.notBefore) {
150
+ jwtPayload.nbf = typeof options.notBefore === 'number' ? options.notBefore : now + Jwt.parseExpiration(options.notBefore);
151
+ }
152
+ if (options.jwtId) jwtPayload.jti = options.jwtId;
153
+
154
+ const encodedPayload = Jwt.base64UrlEncode(JSON.stringify(jwtPayload));
155
+
156
+ // 创建签名
157
+ const data = `${header}.${encodedPayload}`;
158
+ const signature = Jwt.createSignature(algorithm, secret, data);
159
+
160
+ return `${data}.${signature}`;
161
+ }
162
+
163
+ /**
164
+ * 验证 JWT token
165
+ * @param token - JWT token字符串
166
+ * @param options - 验证选项
167
+ * @returns 解码后的载荷数据
168
+ */
169
+ static verify(token: string, options: JwtVerifyOptions): JwtPayload;
170
+ static verify(token: string, secret: string): JwtPayload;
171
+ static verify(token: string, optionsOrSecret: JwtVerifyOptions | string = {}): JwtPayload {
172
+ if (!token || typeof token !== 'string') {
173
+ throw new Error('Token必须是非空字符串');
174
+ }
175
+
176
+ // 支持两种调用方式
177
+ let options: JwtVerifyOptions;
178
+ if (typeof optionsOrSecret === 'string') {
179
+ // verify(token, secret) 形式
180
+ options = { secret: optionsOrSecret };
181
+ } else {
182
+ // verify(token, options) 形式
183
+ options = optionsOrSecret;
184
+ }
185
+
186
+ const secret = options.secret || Env.JWT_SECRET;
187
+ if (!secret) {
188
+ throw new Error('JWT密钥是必需的');
189
+ }
190
+
191
+ const parts = token.split('.');
192
+ if (parts.length !== 3) {
193
+ throw new Error('JWT格式无效');
194
+ }
195
+
196
+ try {
197
+ // 解析 header 和 payload
198
+ const header = JSON.parse(Jwt.base64UrlDecode(parts[0])) as JwtHeader;
199
+ const payload = JSON.parse(Jwt.base64UrlDecode(parts[1])) as JwtPayload;
200
+ const signature = parts[2];
201
+
202
+ // 验证算法
203
+ if (!Jwt.ALGORITHMS[header.alg]) {
204
+ throw new Error(`不支持的算法: ${header.alg}`);
205
+ }
206
+
207
+ // 验证签名
208
+ const data = `${parts[0]}.${parts[1]}`;
209
+ const expectedSignature = Jwt.createSignature(header.alg, secret, data);
210
+
211
+ if (!Jwt.constantTimeCompare(signature, expectedSignature)) {
212
+ throw new Error('Token签名无效');
213
+ }
214
+
215
+ // 验证时间
216
+ const now = Math.floor(Date.now() / 1000);
217
+
218
+ if (!options.ignoreExpiration && payload.exp && payload.exp < now) {
219
+ throw new Error('Token已过期 (expired)');
220
+ }
221
+ if (!options.ignoreNotBefore && payload.nbf && payload.nbf > now) {
222
+ throw new Error('Token尚未生效');
223
+ }
224
+
225
+ // 验证 issuer、audience、subject
226
+ if (options.issuer && payload.iss !== options.issuer) {
227
+ throw new Error('Token发行者无效');
228
+ }
229
+ if (options.audience) {
230
+ const audiences = Array.isArray(payload.aud) ? payload.aud : [payload.aud];
231
+ if (!audiences.includes(options.audience)) {
232
+ throw new Error('Token受众无效');
233
+ }
234
+ }
235
+ if (options.subject && payload.sub !== options.subject) {
236
+ throw new Error('Token主题无效');
237
+ }
238
+
239
+ return payload;
240
+ } catch (error: any) {
241
+ if (error.message.includes('JWT') || error.message.includes('Token') || error.message.includes('无效') || error.message.includes('过期') || error.message.includes('不支持')) {
242
+ throw error;
243
+ }
244
+ throw new Error('Token验证失败: ' + error.message);
245
+ }
246
+ }
247
+
248
+ /**
249
+ * 解码 JWT token (不验证签名)
250
+ * @param token - JWT token字符串
251
+ * @param complete - 是否返回完整信息(包含header)
252
+ * @returns 解码后的内容
253
+ */
254
+ static decode(token: string, complete?: false): JwtPayload;
255
+ static decode(token: string, complete: true): JwtDecoded;
256
+ static decode(token: string, complete: boolean = false): JwtPayload | JwtDecoded {
257
+ if (!token || typeof token !== 'string') {
258
+ throw new Error('Token必须是非空字符串');
259
+ }
260
+
261
+ const parts = token.split('.');
262
+ if (parts.length !== 3) {
263
+ throw new Error('JWT格式无效');
264
+ }
265
+
266
+ try {
267
+ const header = JSON.parse(Jwt.base64UrlDecode(parts[0])) as JwtHeader;
268
+ const payload = JSON.parse(Jwt.base64UrlDecode(parts[1])) as JwtPayload;
269
+
270
+ return complete ? { header, payload, signature: parts[2] } : payload;
271
+ } catch (error: any) {
272
+ throw new Error('JWT解码失败: ' + error.message);
273
+ }
274
+ }
275
+
276
+ /**
277
+ * 获取 token 剩余有效时间
278
+ * @param token - JWT token字符串
279
+ * @returns 剩余秒数,-1表示已过期或无过期时间
280
+ */
281
+ static getTimeToExpiry(token: string): number {
282
+ try {
283
+ const payload = this.decode(token);
284
+ if (!payload.exp) return -1;
285
+ const remaining = payload.exp - Math.floor(Date.now() / 1000);
286
+ return remaining > 0 ? remaining : -1;
287
+ } catch {
288
+ return -1;
289
+ }
290
+ }
291
+
292
+ /**
293
+ * 检查 token 是否已过期
294
+ * @param token - JWT token字符串
295
+ * @returns 是否已过期
296
+ */
297
+ static isExpired(token: string): boolean {
298
+ return this.getTimeToExpiry(token) <= 0;
299
+ }
300
+
301
+ // ==================== 应用层便捷方法 ====================
302
+
303
+ /**
304
+ * 签名用户认证 token
305
+ */
306
+ static signUserToken(userInfo: JwtPayload, options: JwtSignOptions = {}): string {
307
+ return this.sign(userInfo, options);
308
+ }
309
+
310
+ /**
311
+ * 签名 API 访问 token
312
+ */
313
+ static signAPIToken(payload: JwtPayload, options: JwtSignOptions = {}): string {
314
+ return this.sign(payload, { audience: 'api', expiresIn: '1h', ...options });
315
+ }
316
+
317
+ /**
318
+ * 签名刷新 token
319
+ */
320
+ static signRefreshToken(payload: JwtPayload, options: JwtSignOptions = {}): string {
321
+ return this.sign(payload, { audience: 'refresh', expiresIn: '30d', ...options });
322
+ }
323
+
324
+ /**
325
+ * 签名临时 token (用于重置密码等)
326
+ */
327
+ static signTempToken(payload: JwtPayload, options: JwtSignOptions = {}): string {
328
+ return this.sign(payload, { audience: 'temporary', expiresIn: '15m', ...options });
329
+ }
330
+
331
+ /**
332
+ * 验证用户认证 token
333
+ */
334
+ static verifyUserToken(token: string, options: JwtVerifyOptions = {}): JwtPayload {
335
+ return this.verify(token, options);
336
+ }
337
+
338
+ /**
339
+ * 验证 API 访问 token
340
+ */
341
+ static verifyAPIToken(token: string, options: JwtVerifyOptions = {}): JwtPayload {
342
+ return this.verify(token, { audience: 'api', ...options });
343
+ }
344
+
345
+ /**
346
+ * 验证刷新 token
347
+ */
348
+ static verifyRefreshToken(token: string, options: JwtVerifyOptions = {}): JwtPayload {
349
+ return this.verify(token, { audience: 'refresh', ...options });
350
+ }
351
+
352
+ /**
353
+ * 验证临时 token
354
+ */
355
+ static verifyTempToken(token: string, options: JwtVerifyOptions = {}): JwtPayload {
356
+ return this.verify(token, { audience: 'temporary', ...options });
357
+ }
358
+
359
+ /**
360
+ * 验证 token 并检查权限
361
+ */
362
+ static verifyWithPermissions(token: string, requiredPermissions: string | string[], options: JwtVerifyOptions = {}): JwtPayload {
363
+ const payload = this.verify(token, options);
364
+
365
+ if (!payload.permissions) {
366
+ throw new Error('Token中不包含权限信息');
367
+ }
368
+
369
+ const permissions = Array.isArray(requiredPermissions) ? requiredPermissions : [requiredPermissions];
370
+ const hasPermission = permissions.every((permission) => payload.permissions.includes(permission));
371
+
372
+ if (!hasPermission) {
373
+ throw new Error('权限不足');
374
+ }
375
+
376
+ return payload;
377
+ }
378
+
379
+ /**
380
+ * 验证 token 并检查角色
381
+ */
382
+ static verifyWithRoles(token: string, requiredRoles: string | string[], options: JwtVerifyOptions = {}): JwtPayload {
383
+ const payload = this.verify(token, options);
384
+
385
+ if (!payload.role && !payload.roles) {
386
+ throw new Error('Token中不包含角色信息');
387
+ }
388
+
389
+ const userRoles = payload.roles || [payload.role];
390
+ const roles = Array.isArray(requiredRoles) ? requiredRoles : [requiredRoles];
391
+
392
+ const hasRole = roles.some((role) => userRoles.includes(role));
393
+
394
+ if (!hasRole) {
395
+ throw new Error('角色权限不足');
396
+ }
397
+
398
+ return payload;
399
+ }
400
+
401
+ /**
402
+ * 软验证 token (忽略过期时间)
403
+ */
404
+ static verifySoft(token: string, options: JwtVerifyOptions = {}): JwtPayload {
405
+ return this.verify(token, { ignoreExpiration: true, ...options });
406
+ }
407
+
408
+ /**
409
+ * 检查 token 是否即将过期
410
+ * @param token - JWT token字符串
411
+ * @param thresholdSeconds - 过期阈值(秒),默认300秒(5分钟)
412
+ * @returns 是否即将过期
413
+ */
414
+ static isNearExpiry(token: string, thresholdSeconds: number = 300): boolean {
415
+ const timeToExpiry = this.getTimeToExpiry(token);
416
+ return timeToExpiry > 0 && timeToExpiry <= thresholdSeconds;
417
+ }
418
+
419
+ // ==================== 便捷静态方法 ====================
420
+
421
+ /**
422
+ * 创建 token(使用默认配置)
423
+ * @param payload - 载荷数据
424
+ * @returns JWT token字符串
425
+ */
426
+ static create(payload: JwtPayload): string {
427
+ return this.sign(payload, Env.JWT_SECRET, Env.JWT_EXPIRES_IN || '7d');
428
+ }
429
+
430
+ /**
431
+ * 验证 token(使用默认配置)
432
+ * @param token - JWT token字符串
433
+ * @returns 解码后的载荷数据
434
+ */
435
+ static check(token: string): JwtPayload {
436
+ return this.verify(token, { secret: Env.JWT_SECRET });
437
+ }
438
+
439
+ /**
440
+ * 解析 token(不验证签名,使用 decode)
441
+ * @param token - JWT token字符串
442
+ * @returns 解码后的载荷数据
443
+ */
444
+ static parse(token: string): JwtPayload {
445
+ return this.decode(token);
446
+ }
447
+
448
+ /**
449
+ * 检查载荷是否包含特定角色
450
+ * @param payload - JWT载荷
451
+ * @param role - 要检查的角色
452
+ * @returns 是否包含该角色
453
+ */
454
+ static hasRole(payload: JwtPayload, role: string): boolean {
455
+ if (!payload) return false;
456
+ if (payload.role === role) return true;
457
+ if (Array.isArray(payload.roles) && payload.roles.includes(role)) return true;
458
+ return false;
459
+ }
460
+
461
+ /**
462
+ * 检查载荷是否包含任意一个角色
463
+ * @param payload - JWT载荷
464
+ * @param roles - 要检查的角色列表
465
+ * @returns 是否包含任意一个角色
466
+ */
467
+ static hasAnyRole(payload: JwtPayload, roles: string[]): boolean {
468
+ if (!payload) return false;
469
+ return roles.some((role) => this.hasRole(payload, role));
470
+ }
471
+
472
+ /**
473
+ * 检查载荷是否包含特定权限
474
+ * @param payload - JWT载荷
475
+ * @param permission - 要检查的权限
476
+ * @returns 是否包含该权限
477
+ */
478
+ static hasPermission(payload: JwtPayload, permission: string): boolean {
479
+ if (!payload || !payload.permissions) return false;
480
+ return Array.isArray(payload.permissions) && payload.permissions.includes(permission);
481
+ }
482
+
483
+ /**
484
+ * 检查载荷是否包含所有权限
485
+ * @param payload - JWT载荷
486
+ * @param permissions - 要检查的权限列表
487
+ * @returns 是否包含所有权限
488
+ */
489
+ static hasAllPermissions(payload: JwtPayload, permissions: string[]): boolean {
490
+ if (!payload || !payload.permissions) return false;
491
+ return permissions.every((permission) => this.hasPermission(payload, permission));
492
+ }
493
+ }