kb-server 0.0.11 → 0.0.13
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/README.md +426 -18
- package/dist/common/api-middleware.d.ts +0 -1
- package/dist/common/api-middleware.js +18 -12
- package/dist/common/create-server.d.ts +1 -0
- package/dist/common/create-server.js +1 -1
- package/dist/common/sanitize-traceinfo.d.ts +35 -0
- package/dist/common/sanitize-traceinfo.js +115 -0
- package/dist/tests/sanitize-traceinfo.test.d.ts +5 -0
- package/dist/tests/sanitize-traceinfo.test.js +104 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -1,44 +1,452 @@
|
|
|
1
1
|
# KB Server
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
基于 `Express` 的快速 Node.js 服务框架,提供简洁的 API 开发体验、标准错误码、SSE 支持、统一鉴权和完善的日志系统。
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
## 特性
|
|
6
|
+
|
|
7
|
+
- 🚀 快速创建 REST API
|
|
8
|
+
- 🎯 基于类的参数校验(使用 class-validator)
|
|
9
|
+
- 🔒 统一鉴权函数(API 级别)
|
|
10
|
+
- 📡 原生支持 SSE (Server-Sent Events)
|
|
11
|
+
- 📋 标准错误码系统
|
|
12
|
+
- 🔌 支持自定义 Express 中间件
|
|
13
|
+
- 📊 内置日志系统(基于 pino)
|
|
14
|
+
- 🌍 统一的响应格式
|
|
15
|
+
|
|
16
|
+
## 安装
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
npm install kb-server
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
要求 Node.js >= 18.0.0
|
|
23
|
+
|
|
24
|
+
## 快速开始
|
|
25
|
+
|
|
26
|
+
### 基础用法
|
|
6
27
|
|
|
7
28
|
```typescript
|
|
8
29
|
import { createServer } from "kb-server";
|
|
9
30
|
import * as apis from "./apis";
|
|
10
31
|
|
|
11
|
-
//
|
|
32
|
+
// 写法一:直接创建并启动
|
|
12
33
|
const server = createServer({ apis });
|
|
13
34
|
server.listen(3000);
|
|
14
35
|
|
|
15
|
-
//
|
|
36
|
+
// 写法二:异步初始化后启动
|
|
16
37
|
(async () => {
|
|
17
|
-
//
|
|
38
|
+
// 其他异步操作,例如:初始化数据库
|
|
18
39
|
return createServer({ apis });
|
|
19
40
|
})().then((app) => app.listen(3000));
|
|
20
41
|
```
|
|
21
42
|
|
|
22
|
-
|
|
43
|
+
### 创建 API
|
|
44
|
+
|
|
45
|
+
使用 `implementAPI` 创建带有类型安全的 API:
|
|
46
|
+
|
|
47
|
+
```typescript
|
|
48
|
+
import { implementAPI } from "kb-server";
|
|
49
|
+
import { IsString, IsNotEmpty } from "class-validator";
|
|
50
|
+
|
|
51
|
+
// 定义请求参数类
|
|
52
|
+
class CreateUserParams {
|
|
53
|
+
@IsString()
|
|
54
|
+
@IsNotEmpty()
|
|
55
|
+
name: string;
|
|
56
|
+
|
|
57
|
+
@IsString()
|
|
58
|
+
@IsNotEmpty()
|
|
59
|
+
email: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// 定义 API 函数类型
|
|
63
|
+
type CreateUserFn = (params: CreateUserParams) => Promise<{ id: string }>;
|
|
64
|
+
|
|
65
|
+
// 实现 API
|
|
66
|
+
export const createUser = implementAPI<CreateUserFn>(
|
|
67
|
+
{} as CreateUserFn,
|
|
68
|
+
CreateUserParams,
|
|
69
|
+
async (params, ctx) => {
|
|
70
|
+
// ctx.RequestId - 请求 ID
|
|
71
|
+
// ctx.AuthInfo - 鉴权信息(如果配置了 authFn)
|
|
72
|
+
console.log(`Request ID: ${ctx.RequestId}`);
|
|
73
|
+
|
|
74
|
+
// 业务逻辑
|
|
75
|
+
const user = await database.createUser(params);
|
|
76
|
+
|
|
77
|
+
return { id: user.id };
|
|
78
|
+
}
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
// 导出所有 API
|
|
82
|
+
export default {
|
|
83
|
+
CreateUser: createUser,
|
|
84
|
+
};
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 配置服务器
|
|
23
88
|
|
|
24
89
|
```typescript
|
|
25
90
|
import { createServer } from "kb-server";
|
|
91
|
+
import cors from "cors";
|
|
26
92
|
import * as apis from "./apis";
|
|
27
93
|
import * as sse from "./sse";
|
|
28
94
|
|
|
95
|
+
const server = createServer(
|
|
96
|
+
{
|
|
97
|
+
apis, // API 列表
|
|
98
|
+
authFn, // 鉴权函数(可选)
|
|
99
|
+
log: true, // 是否开启请求日志
|
|
100
|
+
middlewares: [cors()], // 自定义中间件列表
|
|
101
|
+
sse: { // SSE 配置(可选)
|
|
102
|
+
handlers: sse, // SSE 处理函数
|
|
103
|
+
route: "/sse", // SSE 路由(默认: /sse)
|
|
104
|
+
timeout: 30000, // 超时时间(毫秒,默认: 30000)
|
|
105
|
+
},
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
limit: "10mb", // 请求体大小限制(默认: 10mb)
|
|
109
|
+
}
|
|
110
|
+
);
|
|
29
111
|
|
|
30
|
-
(
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
112
|
+
server.listen(3000);
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
## SSE 支持
|
|
116
|
+
|
|
117
|
+
创建 SSE API 实现实时数据推送:
|
|
118
|
+
|
|
119
|
+
```typescript
|
|
120
|
+
import { implementSseAPI } from "kb-server";
|
|
121
|
+
import { IsString } from "class-validator";
|
|
122
|
+
|
|
123
|
+
// 定义请求参数
|
|
124
|
+
class StreamChatParams {
|
|
125
|
+
@IsString()
|
|
126
|
+
prompt: string;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// 定义 SSE 函数类型
|
|
130
|
+
type StreamChatFn = (params: StreamChatParams) => Promise<void>;
|
|
131
|
+
|
|
132
|
+
// 实现 SSE API
|
|
133
|
+
export const streamChat = implementSseAPI<StreamChatFn>(
|
|
134
|
+
{} as StreamChatFn,
|
|
135
|
+
StreamChatParams,
|
|
136
|
+
async (params, sse, ctx) => {
|
|
137
|
+
// sse.push(data) - 推送数据到客户端
|
|
138
|
+
// sse.close() - 主动关闭连接
|
|
139
|
+
// sse.abortController - 中止控制器
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
// 模拟流式推送
|
|
143
|
+
for (let i = 0; i < 10; i++) {
|
|
144
|
+
sse.push({
|
|
145
|
+
chunk: `消息片段 ${i + 1}`,
|
|
146
|
+
progress: (i + 1) * 10,
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
// 模拟延迟
|
|
150
|
+
await new Promise(resolve => setTimeout(resolve, 500));
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// 完成后关闭连接
|
|
154
|
+
sse.close();
|
|
155
|
+
} catch (error) {
|
|
156
|
+
// 可以使用 abortController 中止操作
|
|
157
|
+
if (!sse.abortController.signal.aborted) {
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
|
|
164
|
+
// 导出 SSE API
|
|
165
|
+
export default {
|
|
166
|
+
StreamChat: streamChat,
|
|
167
|
+
};
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
## 统一鉴权
|
|
171
|
+
|
|
172
|
+
在 API 级别实现统一的鉴权逻辑:
|
|
173
|
+
|
|
174
|
+
```typescript
|
|
175
|
+
import { createServer } from "kb-server";
|
|
176
|
+
|
|
177
|
+
const authFn = async (action: string, req: express.Request) => {
|
|
178
|
+
// 从请求中获取 token
|
|
179
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
180
|
+
|
|
181
|
+
if (!token) {
|
|
182
|
+
return false; // 返回 false 表示无权限
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// 验证 token
|
|
186
|
+
const userInfo = await verifyToken(token);
|
|
187
|
+
if (!userInfo) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// 返回对象会将信息注入到 ctx.AuthInfo
|
|
192
|
+
return {
|
|
193
|
+
userId: userInfo.id,
|
|
194
|
+
role: userInfo.role,
|
|
195
|
+
};
|
|
196
|
+
};
|
|
197
|
+
|
|
198
|
+
const server = createServer({
|
|
199
|
+
apis,
|
|
200
|
+
authFn,
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
// 在 API 中使用鉴权信息
|
|
204
|
+
const myAPI = implementAPI<SomeFn>(
|
|
205
|
+
{} as SomeFn,
|
|
206
|
+
ParamsClass,
|
|
207
|
+
async (params, ctx) => {
|
|
208
|
+
const { userId, role } = ctx.AuthInfo || {};
|
|
209
|
+
console.log(`当前用户: ${userId}, 角色: ${role}`);
|
|
210
|
+
|
|
211
|
+
// 业务逻辑...
|
|
212
|
+
}
|
|
213
|
+
);
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
## 自定义错误码
|
|
217
|
+
|
|
218
|
+
使用 `createErrors` 创建业务特定的错误码:
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
import { createErrors } from "kb-server";
|
|
222
|
+
|
|
223
|
+
// 创建自定义错误码
|
|
224
|
+
const CustomErrors = createErrors({
|
|
225
|
+
// 一级错误码(可选)
|
|
226
|
+
// InvalidParameter: {
|
|
227
|
+
// // 扩展默认错误码
|
|
228
|
+
// },
|
|
229
|
+
BusinessError: {
|
|
230
|
+
// 新增业务错误码
|
|
231
|
+
InsufficientBalance: "余额不足",
|
|
232
|
+
OrderExpired: "订单已过期",
|
|
233
|
+
ProductOutOfStock: "商品库存不足",
|
|
234
|
+
},
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
// 使用自定义错误
|
|
238
|
+
const someAPI = implementAPI<SomeFn>(
|
|
239
|
+
{} as SomeFn,
|
|
240
|
+
ParamsClass,
|
|
241
|
+
async (params, ctx) => {
|
|
242
|
+
const balance = await getBalance(ctx.AuthInfo?.userId);
|
|
243
|
+
if (balance < 100) {
|
|
244
|
+
throw new CustomErrors.BusinessError.InsufficientBalance();
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// 业务逻辑...
|
|
248
|
+
}
|
|
249
|
+
);
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## 请求与响应格式
|
|
253
|
+
|
|
254
|
+
### 请求格式
|
|
255
|
+
|
|
256
|
+
所有 POST 请求体应包含以下结构:
|
|
257
|
+
|
|
258
|
+
```json
|
|
259
|
+
{
|
|
260
|
+
"Action": "API名称",
|
|
261
|
+
"其他参数": "..."
|
|
262
|
+
}
|
|
263
|
+
```
|
|
264
|
+
|
|
265
|
+
### 成功响应
|
|
266
|
+
|
|
267
|
+
```json
|
|
268
|
+
{
|
|
269
|
+
"Response": {
|
|
270
|
+
"Data": {
|
|
271
|
+
// 返回的数据
|
|
36
272
|
},
|
|
37
|
-
|
|
38
|
-
|
|
273
|
+
"RequestId": "uuid-v4"
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
### 错误响应
|
|
279
|
+
|
|
280
|
+
```json
|
|
281
|
+
{
|
|
282
|
+
"Response": {
|
|
283
|
+
"Error": {
|
|
284
|
+
"Code": "InvalidParameter.ValidationError",
|
|
285
|
+
"Message": "参数校验失败"
|
|
39
286
|
},
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
})().then((app) => app.listen(3000));
|
|
287
|
+
"RequestId": "uuid-v4"
|
|
288
|
+
}
|
|
289
|
+
}
|
|
44
290
|
```
|
|
291
|
+
|
|
292
|
+
## 内置错误码
|
|
293
|
+
|
|
294
|
+
| 一级错误码 | 二级错误码 | 说明 |
|
|
295
|
+
|-----------|-----------|------|
|
|
296
|
+
| InvalidParameter | EmptyParameter | 请求参数不能为空 |
|
|
297
|
+
| InvalidParameter | EmptyAPIRequest | 未指定 API 的请求 |
|
|
298
|
+
| InvalidParameter | ValidationError | 参数校验失败 |
|
|
299
|
+
| InvalidParameter | RouteError | 路由错误 |
|
|
300
|
+
| ResourceNotFound | APINotFound | 不存在的 API |
|
|
301
|
+
| ResourceNotFound | AuthFunctionNotFound | 鉴权函数不存在 |
|
|
302
|
+
| FailOperation | NoPermission | 无权操作 |
|
|
303
|
+
| FailOperation | NotLogin | 未登录 |
|
|
304
|
+
| InternalError | UnknownError | 未知错误 |
|
|
305
|
+
| InternalError | ServiceError | 内部服务错误 |
|
|
306
|
+
| InternalError | DatabaseError | DB 异常 |
|
|
307
|
+
|
|
308
|
+
## 日志系统
|
|
309
|
+
|
|
310
|
+
框架内置基于 pino 的日志系统,支持结构化日志:
|
|
311
|
+
|
|
312
|
+
```typescript
|
|
313
|
+
import { logger } from "kb-server";
|
|
314
|
+
|
|
315
|
+
// 使用日志
|
|
316
|
+
logger.info({ userId: 123, action: "login" }, "用户登录");
|
|
317
|
+
logger.warn({ ip: "192.168.1.1" }, "异常访问");
|
|
318
|
+
logger.error({ error: err }, "系统错误");
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
环境变量:
|
|
322
|
+
- `LOG_LEVEL` - 日志级别(默认: info)
|
|
323
|
+
- `NODE_ENV` - 环境模式(非 production 时使用 pino-pretty 格式化输出)
|
|
324
|
+
|
|
325
|
+
## 完整示例
|
|
326
|
+
|
|
327
|
+
```typescript
|
|
328
|
+
import { createServer, implementAPI, createErrors } from "kb-server";
|
|
329
|
+
import { IsString, IsEmail } from "class-validator";
|
|
330
|
+
import cors from "cors";
|
|
331
|
+
|
|
332
|
+
// 自定义错误码
|
|
333
|
+
const AppErrors = createErrors({
|
|
334
|
+
UserError: {
|
|
335
|
+
DuplicateEmail: "邮箱已被注册",
|
|
336
|
+
},
|
|
337
|
+
});
|
|
338
|
+
|
|
339
|
+
// 参数类
|
|
340
|
+
class RegisterParams {
|
|
341
|
+
@IsString()
|
|
342
|
+
@IsNotEmpty()
|
|
343
|
+
username: string;
|
|
344
|
+
|
|
345
|
+
@IsString()
|
|
346
|
+
@IsEmail()
|
|
347
|
+
email: string;
|
|
348
|
+
|
|
349
|
+
@IsString()
|
|
350
|
+
@IsNotEmpty()
|
|
351
|
+
password: string;
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// 定义 API 类型
|
|
355
|
+
type RegisterFn = (params: RegisterParams) => Promise<{ id: string }>;
|
|
356
|
+
|
|
357
|
+
// 实现 API
|
|
358
|
+
export const register = implementAPI<RegisterFn>(
|
|
359
|
+
{} as RegisterFn,
|
|
360
|
+
RegisterParams,
|
|
361
|
+
async (params, ctx) => {
|
|
362
|
+
// 检查邮箱是否已注册
|
|
363
|
+
const exists = await checkEmailExists(params.email);
|
|
364
|
+
if (exists) {
|
|
365
|
+
throw new AppErrors.UserError.DuplicateEmail();
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 创建用户
|
|
369
|
+
const user = await createUser(params);
|
|
370
|
+
|
|
371
|
+
return { id: user.id };
|
|
372
|
+
}
|
|
373
|
+
);
|
|
374
|
+
|
|
375
|
+
// 导出 API
|
|
376
|
+
const apis = {
|
|
377
|
+
User_Register: register,
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
// 鉴权函数
|
|
381
|
+
const authFn = async (action, req) => {
|
|
382
|
+
// 公开接口不需要鉴权
|
|
383
|
+
if (action.startsWith("User_")) {
|
|
384
|
+
return true;
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// 其他接口需要鉴权
|
|
388
|
+
const token = req.headers.authorization?.replace('Bearer ', '');
|
|
389
|
+
return await verifyToken(token);
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// 创建服务器
|
|
393
|
+
const server = createServer({
|
|
394
|
+
apis,
|
|
395
|
+
authFn,
|
|
396
|
+
log: true,
|
|
397
|
+
middlewares: [cors()],
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
server.listen(3000, () => {
|
|
401
|
+
console.log('Server is running on http://localhost:3000');
|
|
402
|
+
});
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
## API 参考
|
|
406
|
+
|
|
407
|
+
### createServer
|
|
408
|
+
|
|
409
|
+
创建 Express 服务器实例。
|
|
410
|
+
|
|
411
|
+
```typescript
|
|
412
|
+
function createServer(
|
|
413
|
+
params: ICreateServerParams,
|
|
414
|
+
options?: ICreateServerOptions
|
|
415
|
+
): express.Application
|
|
416
|
+
```
|
|
417
|
+
|
|
418
|
+
**参数**:
|
|
419
|
+
- `params.apis` - API 列表
|
|
420
|
+
- `params.authFn` - 鉴权函数(可选)
|
|
421
|
+
- `params.log` - 是否开启日志(默认: true)
|
|
422
|
+
- `params.middlewares` - 自定义中间件列表(可选)
|
|
423
|
+
- `params.sse` - SSE 配置(可选)
|
|
424
|
+
- `options.limit` - 请求体大小限制(默认: "10mb")
|
|
425
|
+
|
|
426
|
+
### implementAPI
|
|
427
|
+
|
|
428
|
+
实现带有类型检查的 API。
|
|
429
|
+
|
|
430
|
+
```typescript
|
|
431
|
+
function implementAPI<F, A = Record<string, any>>(
|
|
432
|
+
actionStub: F,
|
|
433
|
+
ParamsClass: Class<StubParam<F>>,
|
|
434
|
+
execution: APIExecution<StubParam<F>, ReturnType<F>, A>
|
|
435
|
+
): API<StubParam<F>, ReturnType<F>>
|
|
436
|
+
```
|
|
437
|
+
|
|
438
|
+
### implementSseAPI
|
|
439
|
+
|
|
440
|
+
实现 SSE API。
|
|
441
|
+
|
|
442
|
+
```typescript
|
|
443
|
+
function implementSseAPI<F, A = Record<string, any>>(
|
|
444
|
+
actionStub: F,
|
|
445
|
+
ParamsClass: Class<StubParam<F>>,
|
|
446
|
+
execution: SseExecution<StubParam<F>, ReturnType<F>, A>
|
|
447
|
+
): API<StubParam<F>, ReturnType<F>>
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
## License
|
|
451
|
+
|
|
452
|
+
ISC
|
|
@@ -26,7 +26,6 @@ export interface APIErrorResponse {
|
|
|
26
26
|
export type AuthFunction = (action: string, req: express.Request) => Promise<Record<string, any> | boolean> | boolean | Record<string, any>;
|
|
27
27
|
interface IPackAPIOptions {
|
|
28
28
|
authFn?: AuthFunction;
|
|
29
|
-
log?: boolean;
|
|
30
29
|
}
|
|
31
30
|
/**
|
|
32
31
|
* API包装函数
|
|
@@ -4,13 +4,14 @@ exports.packAPI = void 0;
|
|
|
4
4
|
const create_errors_1 = require("./create-errors");
|
|
5
5
|
const uuid_1 = require("uuid");
|
|
6
6
|
const logger_1 = require("../helper/logger");
|
|
7
|
+
const sanitize_traceinfo_1 = require("./sanitize-traceinfo");
|
|
7
8
|
/**
|
|
8
9
|
* API包装函数
|
|
9
10
|
* @param apis API列表
|
|
10
11
|
* @returns
|
|
11
12
|
*/
|
|
12
13
|
const packAPI = (apis, options) => {
|
|
13
|
-
const { authFn
|
|
14
|
+
const { authFn } = options || {};
|
|
14
15
|
return async (req, res) => {
|
|
15
16
|
// 生成API映射
|
|
16
17
|
const apiMap = new Map(Object.entries(apis).map(([action, execution]) => [action, execution]));
|
|
@@ -24,19 +25,21 @@ const packAPI = (apis, options) => {
|
|
|
24
25
|
RequestId: requestId,
|
|
25
26
|
};
|
|
26
27
|
// API 解析 - 将Action定义移到try块外部,以便在catch块中也能访问
|
|
27
|
-
const { Action, ...params } = req.body || {};
|
|
28
|
+
const { Action, TraceInfo, ...params } = req.body || {};
|
|
28
29
|
try {
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
30
|
+
// 安全处理TraceInfo
|
|
31
|
+
const safeTraceInfo = TraceInfo ? (0, sanitize_traceinfo_1.sanitizeTraceInfo)(TraceInfo) : undefined;
|
|
32
|
+
logger_1.logger.info({
|
|
33
|
+
requestId,
|
|
34
|
+
action: Action,
|
|
35
|
+
params,
|
|
36
|
+
path: req.path,
|
|
37
|
+
method: req.method,
|
|
38
|
+
traceInfo: safeTraceInfo,
|
|
39
|
+
}, "收到请求");
|
|
38
40
|
// 接口未定义
|
|
39
41
|
if (!Action) {
|
|
42
|
+
logger_1.logger.warn({ requestId, action: Action }, "接口未定义");
|
|
40
43
|
throw new create_errors_1.CommonErrors.InvalidParameter.EmptyAPIRequest();
|
|
41
44
|
}
|
|
42
45
|
// 处理鉴权函数
|
|
@@ -44,8 +47,10 @@ const packAPI = (apis, options) => {
|
|
|
44
47
|
if (typeof authFn !== "function") {
|
|
45
48
|
throw new create_errors_1.CommonErrors.ResourceNotFound.AuthFunctionNotFound();
|
|
46
49
|
}
|
|
50
|
+
logger_1.logger.info({ requestId, action: Action }, "开始执行框架权限检查");
|
|
47
51
|
const authResult = await authFn(Action, req);
|
|
48
52
|
if (!authResult) {
|
|
53
|
+
logger_1.logger.warn({ requestId, action: Action }, "框架权限检查不通过");
|
|
49
54
|
throw new create_errors_1.CommonErrors.FailOperation.NoPermission();
|
|
50
55
|
}
|
|
51
56
|
if (typeof authResult !== "boolean") {
|
|
@@ -53,6 +58,7 @@ const packAPI = (apis, options) => {
|
|
|
53
58
|
ctx.AuthInfo = authResult || {};
|
|
54
59
|
}
|
|
55
60
|
}
|
|
61
|
+
logger_1.logger.info({ requestId, action: Action, authInfo: ctx.AuthInfo }, "框架权限检查通过");
|
|
56
62
|
// API 处理
|
|
57
63
|
const execution = apiMap.get(Action);
|
|
58
64
|
if (typeof execution !== "function") {
|
|
@@ -69,7 +75,7 @@ const packAPI = (apis, options) => {
|
|
|
69
75
|
};
|
|
70
76
|
// 完成响应
|
|
71
77
|
took = Date.now() - start;
|
|
72
|
-
logger_1.logger.info({ requestId, action: Action, response, took }, "
|
|
78
|
+
logger_1.logger.info({ requestId, action: Action, response, took }, "执行响应");
|
|
73
79
|
return res.send(response);
|
|
74
80
|
}
|
|
75
81
|
catch (rawError) {
|
|
@@ -43,7 +43,7 @@ function createServer(params, options) {
|
|
|
43
43
|
// 注入SSE
|
|
44
44
|
handlers && app.use((0, sse_middleware_1.packSSE)(handlers, { authFn, log, ...resetSSE }));
|
|
45
45
|
// 注入API
|
|
46
|
-
app.use((0, api_middleware_1.packAPI)(apis, { authFn
|
|
46
|
+
app.use((0, api_middleware_1.packAPI)(apis, { authFn }));
|
|
47
47
|
return app;
|
|
48
48
|
}
|
|
49
49
|
exports.createServer = createServer;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 安全处理工具模块
|
|
3
|
+
* 用于对用户输入的数据进行安全清理,防止日志注入、敏感信息泄露等问题
|
|
4
|
+
*/
|
|
5
|
+
/**
|
|
6
|
+
* 安全处理配置选项
|
|
7
|
+
*/
|
|
8
|
+
export interface SanitizeOptions {
|
|
9
|
+
/** 最大嵌套深度,防止循环引用 */
|
|
10
|
+
maxDepth?: number;
|
|
11
|
+
/** 最大数组长度,防止数据过大 */
|
|
12
|
+
maxArrayLength?: number;
|
|
13
|
+
/** 最大对象属性数量 */
|
|
14
|
+
maxObjectKeys?: number;
|
|
15
|
+
/** 最大字符串长度 */
|
|
16
|
+
maxStringLength?: number;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* 安全处理TraceInfo,防止安全漏洞
|
|
20
|
+
*
|
|
21
|
+
* @param traceInfo - 需要清理的数据
|
|
22
|
+
* @param options - 配置选项
|
|
23
|
+
* @returns 清理后的安全数据
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* const safeData = sanitizeTraceInfo({
|
|
28
|
+
* username: 'test',
|
|
29
|
+
* password: 'secret',
|
|
30
|
+
* apiTrace: []
|
|
31
|
+
* });
|
|
32
|
+
* // 结果: { username: 'test', password: '[REDACTED]', apiTrace: [] }
|
|
33
|
+
* ```
|
|
34
|
+
*/
|
|
35
|
+
export declare function sanitizeTraceInfo(traceInfo: unknown, options?: SanitizeOptions): any;
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* 安全处理工具模块
|
|
4
|
+
* 用于对用户输入的数据进行安全清理,防止日志注入、敏感信息泄露等问题
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
exports.sanitizeTraceInfo = void 0;
|
|
8
|
+
/**
|
|
9
|
+
* 需要脱敏的敏感字段列表
|
|
10
|
+
*/
|
|
11
|
+
const SENSITIVE_KEYS = [
|
|
12
|
+
'password',
|
|
13
|
+
'token',
|
|
14
|
+
'secret',
|
|
15
|
+
'key',
|
|
16
|
+
'auth',
|
|
17
|
+
'credential',
|
|
18
|
+
'cookie',
|
|
19
|
+
];
|
|
20
|
+
/**
|
|
21
|
+
* 默认配置
|
|
22
|
+
*/
|
|
23
|
+
const DEFAULT_OPTIONS = {
|
|
24
|
+
maxDepth: 5,
|
|
25
|
+
maxArrayLength: 100,
|
|
26
|
+
maxObjectKeys: 50,
|
|
27
|
+
maxStringLength: 1000,
|
|
28
|
+
};
|
|
29
|
+
/**
|
|
30
|
+
* 安全处理TraceInfo,防止安全漏洞
|
|
31
|
+
*
|
|
32
|
+
* @param traceInfo - 需要清理的数据
|
|
33
|
+
* @param options - 配置选项
|
|
34
|
+
* @returns 清理后的安全数据
|
|
35
|
+
*
|
|
36
|
+
* @example
|
|
37
|
+
* ```typescript
|
|
38
|
+
* const safeData = sanitizeTraceInfo({
|
|
39
|
+
* username: 'test',
|
|
40
|
+
* password: 'secret',
|
|
41
|
+
* apiTrace: []
|
|
42
|
+
* });
|
|
43
|
+
* // 结果: { username: 'test', password: '[REDACTED]', apiTrace: [] }
|
|
44
|
+
* ```
|
|
45
|
+
*/
|
|
46
|
+
function sanitizeTraceInfo(traceInfo, options = {}) {
|
|
47
|
+
const opts = { ...DEFAULT_OPTIONS, ...options };
|
|
48
|
+
return sanitizeValue(traceInfo, opts, 0);
|
|
49
|
+
}
|
|
50
|
+
exports.sanitizeTraceInfo = sanitizeTraceInfo;
|
|
51
|
+
/**
|
|
52
|
+
* 递归清理数据
|
|
53
|
+
*/
|
|
54
|
+
function sanitizeValue(value, options, currentDepth) {
|
|
55
|
+
// 深度限制,防止循环引用导致的栈溢出
|
|
56
|
+
if (currentDepth > options.maxDepth) {
|
|
57
|
+
return '[MaxDepthReached]';
|
|
58
|
+
}
|
|
59
|
+
// 空值处理
|
|
60
|
+
if (value === null || value === undefined) {
|
|
61
|
+
return value;
|
|
62
|
+
}
|
|
63
|
+
// 原始类型直接处理字符串
|
|
64
|
+
if (typeof value !== 'object') {
|
|
65
|
+
return sanitizeString(value, options.maxStringLength);
|
|
66
|
+
}
|
|
67
|
+
// 数组处理
|
|
68
|
+
if (Array.isArray(value)) {
|
|
69
|
+
return value.map((item, index) => {
|
|
70
|
+
if (index >= options.maxArrayLength) {
|
|
71
|
+
return `[Array truncated, total length: ${value.length}]`;
|
|
72
|
+
}
|
|
73
|
+
return sanitizeValue(item, options, currentDepth + 1);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
// 对象处理
|
|
77
|
+
const sanitized = {};
|
|
78
|
+
let keyCount = 0;
|
|
79
|
+
for (const key of Object.keys(value)) {
|
|
80
|
+
// 限制对象属性数量
|
|
81
|
+
if (keyCount >= options.maxObjectKeys) {
|
|
82
|
+
sanitized['__truncated__'] = `Object truncated, total keys: ${Object.keys(value).length}`;
|
|
83
|
+
break;
|
|
84
|
+
}
|
|
85
|
+
keyCount++;
|
|
86
|
+
const lowerKey = key.toLowerCase();
|
|
87
|
+
// 敏感字段脱敏
|
|
88
|
+
if (SENSITIVE_KEYS.some((sk) => lowerKey.includes(sk))) {
|
|
89
|
+
sanitized[key] = '[REDACTED]';
|
|
90
|
+
}
|
|
91
|
+
else {
|
|
92
|
+
sanitized[key] = sanitizeValue(value[key], options, currentDepth + 1);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
return sanitized;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* 清理字符串值,防止日志注入
|
|
99
|
+
*
|
|
100
|
+
* @param value - 需要清理的值
|
|
101
|
+
* @param maxLength - 最大长度限制
|
|
102
|
+
* @returns 清理后的字符串
|
|
103
|
+
*/
|
|
104
|
+
function sanitizeString(value, maxLength) {
|
|
105
|
+
if (typeof value !== 'string') {
|
|
106
|
+
return value;
|
|
107
|
+
}
|
|
108
|
+
// 移除控制字符和转义特殊字符,防止日志注入
|
|
109
|
+
return value
|
|
110
|
+
.replace(/[\x00-\x1F\x7F]/g, '') // 移除控制字符
|
|
111
|
+
.replace(/\n/g, '\\n') // 转义换行
|
|
112
|
+
.replace(/\r/g, '\\r') // 转义回车
|
|
113
|
+
.replace(/\t/g, '\\t') // 转义制表符
|
|
114
|
+
.substring(0, maxLength); // 限制字符串长度
|
|
115
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* TraceInfo安全处理测试
|
|
4
|
+
* 运行方式: npx tsx src/tests/sanitize-traceinfo.test.ts
|
|
5
|
+
*/
|
|
6
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
7
|
+
const sanitize_traceinfo_1 = require("../common/sanitize-traceinfo");
|
|
8
|
+
console.log("=== TraceInfo安全处理测试 ===\n");
|
|
9
|
+
// 测试1: 敏感字段脱敏
|
|
10
|
+
console.log("测试1: 敏感字段脱敏");
|
|
11
|
+
const test1 = {
|
|
12
|
+
username: 'testuser',
|
|
13
|
+
password: 'secret123',
|
|
14
|
+
accessToken: 'token123',
|
|
15
|
+
normalField: 'normal value'
|
|
16
|
+
};
|
|
17
|
+
const result1 = (0, sanitize_traceinfo_1.sanitizeTraceInfo)(test1);
|
|
18
|
+
console.log("输入:", test1);
|
|
19
|
+
console.log("输出:", result1);
|
|
20
|
+
console.log("✓ password已脱敏:", result1.password === '[REDACTED]');
|
|
21
|
+
console.log("✓ accessToken已脱敏:", result1.accessToken === '[REDACTED]');
|
|
22
|
+
console.log("✓ normalField保留:", result1.normalField === 'normal value');
|
|
23
|
+
console.log("\n---\n");
|
|
24
|
+
// 测试2: 日志注入防护
|
|
25
|
+
console.log("测试2: 日志注入防护(控制字符清理)");
|
|
26
|
+
const test2 = {
|
|
27
|
+
name: 'test\x00name',
|
|
28
|
+
description: 'line1\nline2\rline3\ttab'
|
|
29
|
+
};
|
|
30
|
+
const result2 = (0, sanitize_traceinfo_1.sanitizeTraceInfo)(test2);
|
|
31
|
+
console.log("输入:", test2);
|
|
32
|
+
console.log("输出:", result2);
|
|
33
|
+
console.log("✓ 控制字符已移除:", !result2.name.includes('\x00'));
|
|
34
|
+
console.log("✓ 换行符已转义:", result2.description.includes('\\n'));
|
|
35
|
+
console.log("\n---\n");
|
|
36
|
+
// 测试3: 大小限制
|
|
37
|
+
console.log("测试3: 大小限制(数组和对象)");
|
|
38
|
+
const test3 = {
|
|
39
|
+
items: Array.from({ length: 200 }, (_, i) => i),
|
|
40
|
+
largeObject: {}
|
|
41
|
+
};
|
|
42
|
+
for (let i = 0; i < 100; i++) {
|
|
43
|
+
test3.largeObject[`key${i}`] = i;
|
|
44
|
+
}
|
|
45
|
+
const result3 = (0, sanitize_traceinfo_1.sanitizeTraceInfo)(test3);
|
|
46
|
+
console.log("数组长度限制:", result3.items.length, "(原始200, 限制100)");
|
|
47
|
+
console.log("对象属性数量:", Object.keys(result3).length, "(包含截断标记)");
|
|
48
|
+
console.log("✓ 数组已截断:", result3.items.length === 100);
|
|
49
|
+
console.log("✓ 对象已截断:", result3.largeObject.__truncated__ !== undefined);
|
|
50
|
+
console.log("\n---\n");
|
|
51
|
+
// 测试4: 深度限制
|
|
52
|
+
console.log("测试4: 深度限制");
|
|
53
|
+
const createDeepObject = (depth) => {
|
|
54
|
+
if (depth === 0)
|
|
55
|
+
return 'value';
|
|
56
|
+
return { level: createDeepObject(depth - 1) };
|
|
57
|
+
};
|
|
58
|
+
const test4 = createDeepObject(10);
|
|
59
|
+
const result4 = (0, sanitize_traceinfo_1.sanitizeTraceInfo)(test4);
|
|
60
|
+
console.log("✓ 深度限制已生效:", JSON.stringify(result4).includes('[MaxDepthReached]'));
|
|
61
|
+
console.log("\n---\n");
|
|
62
|
+
// 测试5: 字符串长度限制
|
|
63
|
+
console.log("测试5: 字符串长度限制");
|
|
64
|
+
const test5 = { long: 'a'.repeat(2000) };
|
|
65
|
+
const result5 = (0, sanitize_traceinfo_1.sanitizeTraceInfo)(test5);
|
|
66
|
+
console.log("✓ 长字符串已截断:", result5.long.length === 1000);
|
|
67
|
+
console.log("\n---\n");
|
|
68
|
+
// 测试6: 实际TraceInfo场景
|
|
69
|
+
console.log("测试6: 实际TraceInfo场景");
|
|
70
|
+
const test6 = {
|
|
71
|
+
sessionId: 'session_123456',
|
|
72
|
+
userId: 'user_789',
|
|
73
|
+
entryPage: '/home',
|
|
74
|
+
currentPage: '/profile',
|
|
75
|
+
apiTrace: [
|
|
76
|
+
{ action: 'GetUser', pageFrom: '/home', duration: 150, status: 'success' },
|
|
77
|
+
{ action: 'UpdateProfile', pageFrom: '/profile', duration: 200, status: 'success' }
|
|
78
|
+
],
|
|
79
|
+
deviceInfo: {
|
|
80
|
+
ua: 'Mozilla/5.0...',
|
|
81
|
+
platform: 'Mac'
|
|
82
|
+
}
|
|
83
|
+
};
|
|
84
|
+
const result6 = (0, sanitize_traceinfo_1.sanitizeTraceInfo)(test6);
|
|
85
|
+
console.log("输入:", JSON.stringify(test6, null, 2));
|
|
86
|
+
console.log("输出:", JSON.stringify(result6, null, 2));
|
|
87
|
+
console.log("✓ 正常TraceInfo保留完整");
|
|
88
|
+
console.log("\n---\n");
|
|
89
|
+
// 测试7: 自定义配置
|
|
90
|
+
console.log("测试7: 自定义配置");
|
|
91
|
+
const test7 = {
|
|
92
|
+
password: 'secret',
|
|
93
|
+
data: Array.from({ length: 10 }, (_, i) => i)
|
|
94
|
+
};
|
|
95
|
+
const result7 = (0, sanitize_traceinfo_1.sanitizeTraceInfo)(test7, {
|
|
96
|
+
maxDepth: 3,
|
|
97
|
+
maxArrayLength: 5,
|
|
98
|
+
maxObjectKeys: 2,
|
|
99
|
+
maxStringLength: 100
|
|
100
|
+
});
|
|
101
|
+
console.log("自定义配置结果:", result7);
|
|
102
|
+
console.log("✓ 自定义配置生效");
|
|
103
|
+
console.log("\n---\n");
|
|
104
|
+
console.log("✅ 所有测试完成!");
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "kb-server",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.13",
|
|
4
4
|
"description": "A fast server for Node.JS,made by express.",
|
|
5
5
|
"main": "dist/index.js",
|
|
6
6
|
"scripts": {
|
|
7
7
|
"build": "rm -rf ./dist && tsc",
|
|
8
|
-
"release": "npm run build && npm version patch && npm publish"
|
|
8
|
+
"release": "npm run build && npm version patch && npm publish",
|
|
9
|
+
"release:beta": "npm run build && npm version prerelease --preid=beta && npm publish --tag beta"
|
|
9
10
|
},
|
|
10
11
|
"keywords": [
|
|
11
12
|
"2kb",
|