@dangao/bun-server 2.0.2 → 2.0.8
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 +21 -8
- package/dist/config/config-module.d.ts +3 -0
- package/dist/config/config-module.d.ts.map +1 -1
- package/dist/core/context.d.ts +10 -0
- package/dist/core/context.d.ts.map +1 -1
- package/dist/database/service.d.ts +4 -4
- package/dist/database/service.d.ts.map +1 -1
- package/dist/index.js +236 -94
- package/dist/queue/queue-module.d.ts.map +1 -1
- package/dist/validation/decorators.d.ts.map +1 -1
- package/package.json +5 -4
- package/src/ai/providers/anthropic-provider.ts +1 -1
- package/src/ai/providers/google-provider.ts +1 -1
- package/src/ai/providers/ollama-provider.ts +1 -1
- package/src/ai/providers/openai-provider.ts +2 -2
- package/src/auth/jwt.ts +1 -1
- package/src/cache/interceptors.ts +3 -3
- package/src/cache/types.ts +10 -10
- package/src/client/runtime.ts +1 -1
- package/src/config/config-module.ts +46 -14
- package/src/config/service.ts +2 -2
- package/src/controller/param-binder.ts +1 -1
- package/src/conversation/service.ts +1 -1
- package/src/core/application.ts +1 -1
- package/src/core/cluster.ts +4 -4
- package/src/core/context.ts +71 -0
- package/src/dashboard/controller.ts +2 -2
- package/src/database/connection-manager.ts +4 -4
- package/src/database/service.ts +25 -28
- package/src/debug/middleware.ts +2 -2
- package/src/di/module-registry.ts +1 -1
- package/src/error/handler.ts +3 -3
- package/src/events/event-module.ts +4 -4
- package/src/files/static-middleware.ts +2 -2
- package/src/files/storage.ts +1 -1
- package/src/interceptor/builtin/log-interceptor.ts +1 -1
- package/src/mcp/server.ts +1 -1
- package/src/middleware/builtin/error-handler.ts +2 -2
- package/src/middleware/builtin/file-upload.ts +1 -1
- package/src/middleware/builtin/rate-limit.ts +1 -1
- package/src/middleware/builtin/static-file.ts +2 -2
- package/src/prompt/stores/file-store.ts +4 -4
- package/src/queue/queue-module.ts +4 -1
- package/src/request/body-parser.ts +3 -3
- package/src/security/filter.ts +1 -1
- package/src/security/guards/guard-registry.ts +1 -1
- package/src/session/middleware.ts +1 -1
- package/src/session/types.ts +5 -5
- package/src/testing/test-client.ts +1 -1
- package/src/validation/decorators.ts +70 -2
- package/src/validation/rules/common.ts +2 -2
- package/tests/config/config-module-extended.test.ts +24 -0
- package/tests/core/context.test.ts +52 -0
- package/tests/database/database-module.test.ts +87 -0
- package/tests/error/error-handler.test.ts +24 -0
- package/tests/queue/queue-module.test.ts +27 -0
- package/tests/validation/validation.test.ts +18 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"queue-module.d.ts","sourceRoot":"","sources":["../../src/queue/queue-module.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,kBAAkB,EAExB,MAAM,SAAS,CAAC;AAEjB,qBAGa,WAAW;IACtB;;;OAGG;WACW,OAAO,CACnB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,WAAW;
|
|
1
|
+
{"version":3,"file":"queue-module.d.ts","sourceRoot":"","sources":["../../src/queue/queue-module.ts"],"names":[],"mappings":"AAGA,OAAO,EAIL,KAAK,kBAAkB,EAExB,MAAM,SAAS,CAAC;AAEjB,qBAGa,WAAW;IACtB;;;OAGG;WACW,OAAO,CACnB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,WAAW;CA6CtB"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../../src/validation/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAgB5E;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,KAAK,EAAE,wBAAwB,EAAE,GAAG,kBAAkB,CAgBjF;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,UAAe,GAAG,wBAAwB,CAM3E;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,UAAe,GAAG,wBAAwB,CAM3E;AAED,wBAAgB,OAAO,CAAC,OAAO,GAAE,UAAe,GAAG,wBAAwB,
|
|
1
|
+
{"version":3,"file":"decorators.d.ts","sourceRoot":"","sources":["../../src/validation/decorators.ts"],"names":[],"mappings":"AAAA,OAAO,kBAAkB,CAAC;AAC1B,OAAO,KAAK,EAAE,wBAAwB,EAAE,kBAAkB,EAAE,MAAM,SAAS,CAAC;AAgB5E;;;GAGG;AACH,wBAAgB,QAAQ,CAAC,GAAG,KAAK,EAAE,wBAAwB,EAAE,GAAG,kBAAkB,CAgBjF;AAED,MAAM,WAAW,UAAU;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,UAAe,GAAG,wBAAwB,CAM3E;AAED,wBAAgB,QAAQ,CAAC,OAAO,GAAE,UAAe,GAAG,wBAAwB,CAM3E;AAED,wBAAgB,OAAO,CAAC,OAAO,GAAE,UAAe,GAAG,wBAAwB,CA2E1E;AAED,wBAAgB,UAAU,IAAI,wBAAwB,CAOrD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,EAAE,OAAO,GAAE,UAAe,GAAG,wBAAwB,CAMzF;AAED;;GAEG;AACH,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,kBAAkB,EAAE,CAIxG"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dangao/bun-server",
|
|
3
|
-
"version": "2.0.
|
|
3
|
+
"version": "2.0.8",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -49,11 +49,12 @@
|
|
|
49
49
|
"dts": "tsc -p tsconfig.build.json",
|
|
50
50
|
"build": "bun run clean && bun run bundle && bun run dts",
|
|
51
51
|
"prepublishOnly": "bun run build",
|
|
52
|
-
"publish:package": "cp -r ../../README.md ../../LICENSE ../../docs . && bun publish --access public && rm -rf ./README.md ./LICENSE ./docs"
|
|
52
|
+
"publish:package": "cp -r ../../README.md ../../LICENSE ../../docs . && bun publish --access public && rm -rf ./README.md ./LICENSE ./docs",
|
|
53
|
+
"publish:package:github": "cp -r ../../README.md ../../LICENSE ../../docs . && bun pm pack --access public && npm publish *.tgz --provenance --access public && rm -rf ./README.md ./LICENSE ./docs"
|
|
53
54
|
},
|
|
54
55
|
"devDependencies": {
|
|
55
|
-
"typescript": "5.9.3",
|
|
56
|
-
"@types/bun": "
|
|
56
|
+
"typescript": "^5.9.3",
|
|
57
|
+
"@types/bun": "^1.3.10"
|
|
57
58
|
},
|
|
58
59
|
"dependencies": {
|
|
59
60
|
"reflect-metadata": "^0.2.2",
|
|
@@ -151,7 +151,7 @@ export class AnthropicProvider implements LlmProvider {
|
|
|
151
151
|
} else if (parsed.type === 'message_stop') {
|
|
152
152
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ done: true })}\n\n`));
|
|
153
153
|
}
|
|
154
|
-
} catch {
|
|
154
|
+
} catch (_error) {
|
|
155
155
|
// skip
|
|
156
156
|
}
|
|
157
157
|
}
|
|
@@ -139,7 +139,7 @@ export class GoogleProvider implements LlmProvider {
|
|
|
139
139
|
const parts = (candidate?.['content'] as Record<string, unknown>)?.['parts'] as Array<Record<string, unknown>> ?? [];
|
|
140
140
|
const text = parts.map((p) => p['text'] ?? '').join('');
|
|
141
141
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify({ content: text, done: false })}\n\n`));
|
|
142
|
-
} catch {
|
|
142
|
+
} catch (_error) {
|
|
143
143
|
// skip
|
|
144
144
|
}
|
|
145
145
|
}
|
|
@@ -171,7 +171,7 @@ export class OpenAIProvider implements LlmProvider {
|
|
|
171
171
|
const delta = parsed.choices?.[0]?.delta;
|
|
172
172
|
const chunk = { content: delta?.content ?? '', done: false };
|
|
173
173
|
controller.enqueue(encoder.encode(`data: ${JSON.stringify(chunk)}\n\n`));
|
|
174
|
-
} catch {
|
|
174
|
+
} catch (_error) {
|
|
175
175
|
// skip malformed lines
|
|
176
176
|
}
|
|
177
177
|
}
|
|
@@ -229,7 +229,7 @@ export class OpenAIProvider implements LlmProvider {
|
|
|
229
229
|
return parsed as Record<string, unknown>;
|
|
230
230
|
}
|
|
231
231
|
return {};
|
|
232
|
-
} catch {
|
|
232
|
+
} catch (_error) {
|
|
233
233
|
return {};
|
|
234
234
|
}
|
|
235
235
|
}
|
package/src/auth/jwt.ts
CHANGED
|
@@ -120,7 +120,7 @@ export class CacheableInterceptor extends BaseInterceptor {
|
|
|
120
120
|
let cacheService: CacheService;
|
|
121
121
|
try {
|
|
122
122
|
cacheService = container.resolve<CacheService>(CACHE_SERVICE_TOKEN);
|
|
123
|
-
} catch {
|
|
123
|
+
} catch (_error) {
|
|
124
124
|
// 缓存服务未注册,直接执行原方法
|
|
125
125
|
console.warn('[CacheableInterceptor] CacheService not registered, skipping cache');
|
|
126
126
|
return await Promise.resolve(originalMethod.apply(target, args));
|
|
@@ -191,7 +191,7 @@ export class CacheEvictInterceptor extends BaseInterceptor {
|
|
|
191
191
|
let cacheService: CacheService;
|
|
192
192
|
try {
|
|
193
193
|
cacheService = container.resolve<CacheService>(CACHE_SERVICE_TOKEN);
|
|
194
|
-
} catch {
|
|
194
|
+
} catch (_error) {
|
|
195
195
|
// 缓存服务未注册,直接执行原方法
|
|
196
196
|
console.warn('[CacheEvictInterceptor] CacheService not registered, skipping cache eviction');
|
|
197
197
|
return await Promise.resolve(originalMethod.apply(target, args));
|
|
@@ -265,7 +265,7 @@ export class CachePutInterceptor extends BaseInterceptor {
|
|
|
265
265
|
let cacheService: CacheService;
|
|
266
266
|
try {
|
|
267
267
|
cacheService = container.resolve<CacheService>(CACHE_SERVICE_TOKEN);
|
|
268
|
-
} catch {
|
|
268
|
+
} catch (_error) {
|
|
269
269
|
// 缓存服务未注册,直接执行原方法
|
|
270
270
|
console.warn('[CachePutInterceptor] CacheService not registered, skipping cache update');
|
|
271
271
|
return await Promise.resolve(originalMethod.apply(target, args));
|
package/src/cache/types.ts
CHANGED
|
@@ -246,7 +246,7 @@ export class RedisCacheStore implements CacheStore {
|
|
|
246
246
|
}
|
|
247
247
|
try {
|
|
248
248
|
return JSON.parse(value) as T;
|
|
249
|
-
} catch {
|
|
249
|
+
} catch (_error) {
|
|
250
250
|
return undefined;
|
|
251
251
|
}
|
|
252
252
|
}
|
|
@@ -264,7 +264,7 @@ export class RedisCacheStore implements CacheStore {
|
|
|
264
264
|
await this.client.set(this.getKey(key), serialized);
|
|
265
265
|
}
|
|
266
266
|
return true;
|
|
267
|
-
} catch {
|
|
267
|
+
} catch (_error) {
|
|
268
268
|
return false;
|
|
269
269
|
}
|
|
270
270
|
}
|
|
@@ -273,7 +273,7 @@ export class RedisCacheStore implements CacheStore {
|
|
|
273
273
|
try {
|
|
274
274
|
await this.client.del(this.getKey(key));
|
|
275
275
|
return true;
|
|
276
|
-
} catch {
|
|
276
|
+
} catch (_error) {
|
|
277
277
|
return false;
|
|
278
278
|
}
|
|
279
279
|
}
|
|
@@ -282,7 +282,7 @@ export class RedisCacheStore implements CacheStore {
|
|
|
282
282
|
try {
|
|
283
283
|
const result = await this.client.exists(this.getKey(key));
|
|
284
284
|
return result === 1;
|
|
285
|
-
} catch {
|
|
285
|
+
} catch (_error) {
|
|
286
286
|
return false;
|
|
287
287
|
}
|
|
288
288
|
}
|
|
@@ -291,7 +291,7 @@ export class RedisCacheStore implements CacheStore {
|
|
|
291
291
|
try {
|
|
292
292
|
await this.client.flushdb();
|
|
293
293
|
return true;
|
|
294
|
-
} catch {
|
|
294
|
+
} catch (_error) {
|
|
295
295
|
return false;
|
|
296
296
|
}
|
|
297
297
|
}
|
|
@@ -313,12 +313,12 @@ export class RedisCacheStore implements CacheStore {
|
|
|
313
313
|
if (value !== null) {
|
|
314
314
|
try {
|
|
315
315
|
result.set(keys[i], JSON.parse(value) as T);
|
|
316
|
-
} catch {
|
|
316
|
+
} catch (_error) {
|
|
317
317
|
// 忽略解析错误
|
|
318
318
|
}
|
|
319
319
|
}
|
|
320
320
|
}
|
|
321
|
-
} catch {
|
|
321
|
+
} catch (_error) {
|
|
322
322
|
// 忽略错误,返回空 Map
|
|
323
323
|
}
|
|
324
324
|
|
|
@@ -352,7 +352,7 @@ export class RedisCacheStore implements CacheStore {
|
|
|
352
352
|
}
|
|
353
353
|
|
|
354
354
|
return true;
|
|
355
|
-
} catch {
|
|
355
|
+
} catch (_error) {
|
|
356
356
|
return false;
|
|
357
357
|
}
|
|
358
358
|
}
|
|
@@ -372,13 +372,13 @@ export class RedisCacheStore implements CacheStore {
|
|
|
372
372
|
try {
|
|
373
373
|
await this.client.del(key);
|
|
374
374
|
deleted.push(key.replace(this.keyPrefix, ''));
|
|
375
|
-
} catch {
|
|
375
|
+
} catch (_error) {
|
|
376
376
|
// 忽略单个删除错误
|
|
377
377
|
}
|
|
378
378
|
}
|
|
379
379
|
|
|
380
380
|
return deleted;
|
|
381
|
-
} catch {
|
|
381
|
+
} catch (_error) {
|
|
382
382
|
return [];
|
|
383
383
|
}
|
|
384
384
|
}
|
package/src/client/runtime.ts
CHANGED
|
@@ -11,6 +11,23 @@ import { CONFIG_SERVICE_TOKEN, type ConfigModuleOptions } from './types';
|
|
|
11
11
|
providers: [],
|
|
12
12
|
})
|
|
13
13
|
export class ConfigModule {
|
|
14
|
+
private static readonly DANGEROUS_PATH_SEGMENTS = new Set([
|
|
15
|
+
'__proto__',
|
|
16
|
+
'prototype',
|
|
17
|
+
'constructor',
|
|
18
|
+
]);
|
|
19
|
+
|
|
20
|
+
private static isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
21
|
+
if (typeof value !== 'object' || value === null) {
|
|
22
|
+
return false;
|
|
23
|
+
}
|
|
24
|
+
const proto = Object.getPrototypeOf(value);
|
|
25
|
+
return proto === null || proto === Object.prototype;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
private static createSafeContainer(): Record<string, unknown> {
|
|
29
|
+
return Object.create(null) as Record<string, unknown>;
|
|
30
|
+
}
|
|
14
31
|
/**
|
|
15
32
|
* 创建配置模块
|
|
16
33
|
* @param options - 模块配置
|
|
@@ -213,7 +230,7 @@ export class ConfigModule {
|
|
|
213
230
|
try {
|
|
214
231
|
const parsed = ConfigService.parseConfigContent(result.content);
|
|
215
232
|
ConfigModule.setValueByPath(configMap, configPath, parsed);
|
|
216
|
-
} catch {
|
|
233
|
+
} catch (_error) {
|
|
217
234
|
ConfigModule.setValueByPath(configMap, configPath, result.content);
|
|
218
235
|
}
|
|
219
236
|
})
|
|
@@ -261,7 +278,7 @@ export class ConfigModule {
|
|
|
261
278
|
let parsedValue: unknown;
|
|
262
279
|
try {
|
|
263
280
|
parsedValue = ConfigService.parseConfigContent(result.content);
|
|
264
|
-
} catch {
|
|
281
|
+
} catch (_error) {
|
|
265
282
|
parsedValue = result.content;
|
|
266
283
|
}
|
|
267
284
|
|
|
@@ -302,24 +319,39 @@ export class ConfigModule {
|
|
|
302
319
|
path: string,
|
|
303
320
|
value: unknown,
|
|
304
321
|
): void {
|
|
305
|
-
const
|
|
322
|
+
const normalizedSegments = path.split('.').map((segment) => segment.trim());
|
|
323
|
+
if (
|
|
324
|
+
normalizedSegments.length === 0 ||
|
|
325
|
+
normalizedSegments.some((segment) => !segment)
|
|
326
|
+
) {
|
|
327
|
+
throw new Error(`[ConfigModule] Invalid config path: "${path}"`);
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
for (const segment of normalizedSegments) {
|
|
331
|
+
if (ConfigModule.DANGEROUS_PATH_SEGMENTS.has(segment)) {
|
|
332
|
+
throw new Error(`[ConfigModule] Unsafe config path segment: "${segment}"`);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
306
336
|
let current: Record<string, unknown> = obj;
|
|
307
337
|
|
|
308
|
-
for (let i = 0; i <
|
|
309
|
-
const segment =
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
current[segment]
|
|
315
|
-
typeof current[segment] !== 'object'
|
|
316
|
-
) {
|
|
317
|
-
current[segment] = {};
|
|
338
|
+
for (let i = 0; i < normalizedSegments.length - 1; i++) {
|
|
339
|
+
const segment = normalizedSegments[i]!;
|
|
340
|
+
const hasOwn = Object.prototype.hasOwnProperty.call(current, segment);
|
|
341
|
+
const existing = hasOwn ? current[segment] : undefined;
|
|
342
|
+
|
|
343
|
+
if (!ConfigModule.isPlainObject(existing)) {
|
|
344
|
+
current[segment] = ConfigModule.createSafeContainer();
|
|
318
345
|
}
|
|
346
|
+
|
|
319
347
|
current = current[segment] as Record<string, unknown>;
|
|
320
348
|
}
|
|
321
349
|
|
|
322
|
-
|
|
350
|
+
const lastSegment = normalizedSegments[normalizedSegments.length - 1]!;
|
|
351
|
+
if (ConfigModule.DANGEROUS_PATH_SEGMENTS.has(lastSegment)) {
|
|
352
|
+
throw new Error(`[ConfigModule] Unsafe config path segment: "${lastSegment}"`);
|
|
353
|
+
}
|
|
354
|
+
current[lastSegment] = value;
|
|
323
355
|
}
|
|
324
356
|
}
|
|
325
357
|
|
package/src/config/service.ts
CHANGED
|
@@ -24,10 +24,10 @@ export class ConfigService<TConfig extends Record<string, unknown> = Record<stri
|
|
|
24
24
|
|
|
25
25
|
try {
|
|
26
26
|
return JSON.parse(content);
|
|
27
|
-
} catch {
|
|
27
|
+
} catch (_error) {
|
|
28
28
|
try {
|
|
29
29
|
return Bun.JSONC.parse(content);
|
|
30
|
-
} catch {
|
|
30
|
+
} catch (_innerError) {
|
|
31
31
|
return Bun.JSON5.parse(content);
|
|
32
32
|
}
|
|
33
33
|
}
|
|
@@ -125,7 +125,7 @@ export class ConversationService {
|
|
|
125
125
|
for (const msg of newMessages) {
|
|
126
126
|
await this.store.appendMessage(id, msg);
|
|
127
127
|
}
|
|
128
|
-
} catch {
|
|
128
|
+
} catch (_error) {
|
|
129
129
|
// If summarization fails, fall back to simple trim
|
|
130
130
|
await this.store.trim(id, this.maxMessages);
|
|
131
131
|
}
|
package/src/core/application.ts
CHANGED
package/src/core/cluster.ts
CHANGED
|
@@ -117,7 +117,7 @@ export class ClusterManager {
|
|
|
117
117
|
if (this.socketDir) {
|
|
118
118
|
try {
|
|
119
119
|
rmSync(this.socketDir, { recursive: true, force: true });
|
|
120
|
-
} catch {
|
|
120
|
+
} catch (_error) {
|
|
121
121
|
// ignore cleanup errors
|
|
122
122
|
}
|
|
123
123
|
this.socketDir = undefined;
|
|
@@ -186,7 +186,7 @@ export class ClusterManager {
|
|
|
186
186
|
|
|
187
187
|
this.socketDir = join(tmpdir(), `bun-cluster-${process.pid}`);
|
|
188
188
|
// Clean up stale directory from a previous run with the same PID
|
|
189
|
-
try { rmSync(this.socketDir, { recursive: true, force: true }); } catch { /* ignore */ }
|
|
189
|
+
try { rmSync(this.socketDir, { recursive: true, force: true }); } catch (_error) { /* ignore */ }
|
|
190
190
|
mkdirSync(this.socketDir, { recursive: true });
|
|
191
191
|
|
|
192
192
|
this.socketPaths = [];
|
|
@@ -271,7 +271,7 @@ export class ClusterManager {
|
|
|
271
271
|
redirect: 'manual',
|
|
272
272
|
unix: socketPath,
|
|
273
273
|
});
|
|
274
|
-
} catch {
|
|
274
|
+
} catch (_error) {
|
|
275
275
|
return new Response('Bad Gateway', { status: 502 });
|
|
276
276
|
}
|
|
277
277
|
},
|
|
@@ -289,7 +289,7 @@ export class ClusterManager {
|
|
|
289
289
|
);
|
|
290
290
|
|
|
291
291
|
const socketPath = this.socketPaths[index]!;
|
|
292
|
-
try { rmSync(socketPath); } catch { /* ignore */ }
|
|
292
|
+
try { rmSync(socketPath); } catch (_error) { /* ignore */ }
|
|
293
293
|
|
|
294
294
|
this.workers[index] = this.spawnProxyWorker(index, socketPath);
|
|
295
295
|
|
package/src/core/context.ts
CHANGED
|
@@ -8,6 +8,7 @@ import { type URLSearchParams, URL } from 'url';
|
|
|
8
8
|
* 封装 Request 和 Response,提供便捷的访问方法
|
|
9
9
|
*/
|
|
10
10
|
export class Context {
|
|
11
|
+
private static readonly ERROR_REDACTED_KEYS = new Set(['stack', 'trace', 'cause']);
|
|
11
12
|
/**
|
|
12
13
|
* 原始请求对象
|
|
13
14
|
*/
|
|
@@ -225,5 +226,75 @@ export class Context {
|
|
|
225
226
|
...init,
|
|
226
227
|
});
|
|
227
228
|
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* 创建错误响应(自动过滤敏感字段)
|
|
232
|
+
* @param body - 错误响应体
|
|
233
|
+
* @param init - 响应初始化选项
|
|
234
|
+
* @returns Response 对象
|
|
235
|
+
*/
|
|
236
|
+
public createErrorResponse(body?: unknown, init?: ResponseInit): Response {
|
|
237
|
+
const status = init?.status ?? this.statusCode;
|
|
238
|
+
const errorStatus = status >= 400 ? status : 500;
|
|
239
|
+
const sanitizedBody = this.sanitizeErrorPayload(body);
|
|
240
|
+
|
|
241
|
+
return this.createResponse(sanitizedBody, {
|
|
242
|
+
...init,
|
|
243
|
+
status: errorStatus,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
private sanitizeErrorPayload(body: unknown): unknown {
|
|
248
|
+
if (body === undefined || body === null) {
|
|
249
|
+
return body;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
if (body instanceof Error) {
|
|
253
|
+
return {
|
|
254
|
+
error: body.message || 'Internal Server Error',
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
const seen = new WeakSet<object>();
|
|
259
|
+
return this.sanitizeValue(body, seen);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
private sanitizeValue(value: unknown, seen: WeakSet<object>): unknown {
|
|
263
|
+
if (value === null || value === undefined) {
|
|
264
|
+
return value;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (typeof value !== 'object') {
|
|
268
|
+
return value;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (value instanceof Date || value instanceof ArrayBuffer || value instanceof Blob) {
|
|
272
|
+
return value;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (seen.has(value)) {
|
|
276
|
+
return '[Circular]';
|
|
277
|
+
}
|
|
278
|
+
seen.add(value);
|
|
279
|
+
|
|
280
|
+
if (Array.isArray(value)) {
|
|
281
|
+
return value.map((item) => this.sanitizeValue(item, seen));
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const prototype = Object.getPrototypeOf(value);
|
|
285
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
286
|
+
return value;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
const sanitized: Record<string, unknown> = {};
|
|
290
|
+
for (const [key, nestedValue] of Object.entries(value as Record<string, unknown>)) {
|
|
291
|
+
if (Context.ERROR_REDACTED_KEYS.has(key)) {
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
sanitized[key] = this.sanitizeValue(nestedValue, seen);
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
return sanitized;
|
|
298
|
+
}
|
|
228
299
|
}
|
|
229
300
|
|
|
@@ -75,7 +75,7 @@ export class DashboardService {
|
|
|
75
75
|
return (
|
|
76
76
|
username === this.auth.username && password === this.auth.password
|
|
77
77
|
);
|
|
78
|
-
} catch {
|
|
78
|
+
} catch (_error) {
|
|
79
79
|
return false;
|
|
80
80
|
}
|
|
81
81
|
}
|
|
@@ -214,7 +214,7 @@ export class DashboardService {
|
|
|
214
214
|
return new Response(JSON.stringify(data), {
|
|
215
215
|
headers: { 'Content-Type': 'application/json; charset=utf-8' },
|
|
216
216
|
});
|
|
217
|
-
} catch {
|
|
217
|
+
} catch (_error) {
|
|
218
218
|
const data = {
|
|
219
219
|
status: 'up',
|
|
220
220
|
timestamp: Date.now(),
|
|
@@ -104,7 +104,7 @@ export class DatabaseConnectionManager {
|
|
|
104
104
|
return await this.healthCheckMysql(this.currentConnection);
|
|
105
105
|
}
|
|
106
106
|
return false;
|
|
107
|
-
} catch {
|
|
107
|
+
} catch (_error) {
|
|
108
108
|
return false;
|
|
109
109
|
}
|
|
110
110
|
}
|
|
@@ -176,7 +176,7 @@ export class DatabaseConnectionManager {
|
|
|
176
176
|
return true;
|
|
177
177
|
}
|
|
178
178
|
return false;
|
|
179
|
-
} catch {
|
|
179
|
+
} catch (_error) {
|
|
180
180
|
return false;
|
|
181
181
|
}
|
|
182
182
|
}
|
|
@@ -204,7 +204,7 @@ export class DatabaseConnectionManager {
|
|
|
204
204
|
return true;
|
|
205
205
|
}
|
|
206
206
|
return false;
|
|
207
|
-
} catch {
|
|
207
|
+
} catch (_error) {
|
|
208
208
|
return false;
|
|
209
209
|
}
|
|
210
210
|
}
|
|
@@ -232,7 +232,7 @@ export class DatabaseConnectionManager {
|
|
|
232
232
|
return true;
|
|
233
233
|
}
|
|
234
234
|
return false;
|
|
235
|
-
} catch {
|
|
235
|
+
} catch (_error) {
|
|
236
236
|
return false;
|
|
237
237
|
}
|
|
238
238
|
}
|
package/src/database/service.ts
CHANGED
|
@@ -145,7 +145,7 @@ export class DatabaseService {
|
|
|
145
145
|
|
|
146
146
|
/**
|
|
147
147
|
* Bun.SQL 查询实现(PostgreSQL/MySQL)
|
|
148
|
-
*
|
|
148
|
+
* 通过模板字符串调用 Bun.SQL,确保参数走 Bun.SQL 转义逻辑
|
|
149
149
|
*/
|
|
150
150
|
private async queryBunSQL<T = unknown>(
|
|
151
151
|
connection: unknown,
|
|
@@ -153,25 +153,23 @@ export class DatabaseService {
|
|
|
153
153
|
params?: unknown[],
|
|
154
154
|
): Promise<T[]> {
|
|
155
155
|
// Bun.SQL 对象可以作为函数调用(模板字符串)
|
|
156
|
-
// 但我们需要支持参数化查询
|
|
157
156
|
if (connection && typeof connection === 'function') {
|
|
158
|
-
// Bun.SQL 模板字符串查询
|
|
159
|
-
// 注意:Bun.SQL 使用模板字符串,参数会自动转义
|
|
160
|
-
// 这里我们需要将 SQL 和参数组合成模板字符串调用
|
|
161
|
-
// 但由于 TypeScript 限制,我们使用动态调用
|
|
162
157
|
try {
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
158
|
+
const { strings, values } = this.buildTemplateFromSql(sql, params);
|
|
159
|
+
const template = Object.assign(strings, {
|
|
160
|
+
raw: strings,
|
|
161
|
+
}) as unknown as TemplateStringsArray;
|
|
166
162
|
const result = await (connection as (
|
|
167
163
|
template: TemplateStringsArray,
|
|
168
164
|
...values: unknown[]
|
|
169
|
-
) => Promise<Array<Record<string, unknown>>>)
|
|
165
|
+
) => Promise<Array<Record<string, unknown>>>)(template, ...values);
|
|
170
166
|
return result as T[];
|
|
171
|
-
} catch {
|
|
172
|
-
|
|
167
|
+
} catch (error) {
|
|
168
|
+
const errorMessage =
|
|
169
|
+
error instanceof Error ? error.message : String(error);
|
|
170
|
+
// 如果模板字符串方式失败,保留原始错误,便于排查参数/SQL 构造问题
|
|
173
171
|
throw new Error(
|
|
174
|
-
|
|
172
|
+
`Bun.SQL parameterized queries are not fully supported. Consider using template string queries. Original error: ${errorMessage}`,
|
|
175
173
|
);
|
|
176
174
|
}
|
|
177
175
|
}
|
|
@@ -197,25 +195,24 @@ export class DatabaseService {
|
|
|
197
195
|
}
|
|
198
196
|
|
|
199
197
|
/**
|
|
200
|
-
*
|
|
201
|
-
*
|
|
198
|
+
* 将 SQL 与 ? 占位符参数转换为模板字符串片段
|
|
199
|
+
* 让参数通过 Bun.SQL 的 values 通道注入,避免手工拼接 SQL
|
|
202
200
|
*/
|
|
203
|
-
private
|
|
201
|
+
private buildTemplateFromSql(
|
|
202
|
+
sql: string,
|
|
203
|
+
params?: unknown[],
|
|
204
|
+
): { strings: string[]; values: unknown[] } {
|
|
204
205
|
if (!params || params.length === 0) {
|
|
205
|
-
return sql;
|
|
206
|
+
return { strings: [sql], values: [] };
|
|
206
207
|
}
|
|
207
208
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
const value =
|
|
214
|
-
typeof param === 'string'
|
|
215
|
-
? `'${param.replace(/'/g, "''")}'`
|
|
216
|
-
: String(param);
|
|
217
|
-
result = result.replace('?', value);
|
|
209
|
+
const strings = sql.split('?');
|
|
210
|
+
if (strings.length !== params.length + 1) {
|
|
211
|
+
throw new Error(
|
|
212
|
+
'SQL placeholders count does not match parameters count',
|
|
213
|
+
);
|
|
218
214
|
}
|
|
219
|
-
|
|
215
|
+
|
|
216
|
+
return { strings, values: params };
|
|
220
217
|
}
|
|
221
218
|
}
|
package/src/debug/middleware.ts
CHANGED
|
@@ -67,7 +67,7 @@ export function createDebugMiddleware(
|
|
|
67
67
|
if (recordBody && ['POST', 'PUT', 'PATCH'].includes(context.method)) {
|
|
68
68
|
try {
|
|
69
69
|
requestBody = await context.getBody();
|
|
70
|
-
} catch {
|
|
70
|
+
} catch (_error) {
|
|
71
71
|
requestBody = undefined;
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -92,7 +92,7 @@ export function createDebugMiddleware(
|
|
|
92
92
|
if (route) {
|
|
93
93
|
matchedRoute = route.path;
|
|
94
94
|
}
|
|
95
|
-
} catch {
|
|
95
|
+
} catch (_error) {
|
|
96
96
|
matchedRoute = undefined;
|
|
97
97
|
}
|
|
98
98
|
|
|
@@ -242,7 +242,7 @@ export class ModuleRegistry {
|
|
|
242
242
|
} else if ('useFactory' in provider) {
|
|
243
243
|
instances.push(ref.container.resolve(provider.provide as Constructor<unknown>));
|
|
244
244
|
}
|
|
245
|
-
} catch {
|
|
245
|
+
} catch (_error) {
|
|
246
246
|
// skip providers that can't be resolved (e.g. pending async providers)
|
|
247
247
|
}
|
|
248
248
|
}
|
package/src/error/handler.ts
CHANGED
|
@@ -53,12 +53,12 @@ export async function handleError(error: unknown, context: Context): Promise<Res
|
|
|
53
53
|
responseBody.details = error.details;
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
return context.
|
|
56
|
+
return context.createErrorResponse(responseBody);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
if (error instanceof ValidationError) {
|
|
60
60
|
context.setStatus(400);
|
|
61
|
-
return context.
|
|
61
|
+
return context.createErrorResponse({
|
|
62
62
|
error: error.message,
|
|
63
63
|
code: 'VALIDATION_FAILED',
|
|
64
64
|
issues: error.issues,
|
|
@@ -74,7 +74,7 @@ export async function handleError(error: unknown, context: Context): Promise<Res
|
|
|
74
74
|
message,
|
|
75
75
|
stack,
|
|
76
76
|
});
|
|
77
|
-
return context.
|
|
77
|
+
return context.createErrorResponse({
|
|
78
78
|
error: 'Internal Server Error',
|
|
79
79
|
details: process.env.NODE_ENV === 'production' ? undefined : message,
|
|
80
80
|
});
|