@dangao/bun-server 1.12.1 → 2.0.1
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 +32 -0
- package/dist/ai/ai-module.d.ts +24 -0
- package/dist/ai/ai-module.d.ts.map +1 -0
- package/dist/ai/decorators.d.ts +25 -0
- package/dist/ai/decorators.d.ts.map +1 -0
- package/dist/ai/errors.d.ts +39 -0
- package/dist/ai/errors.d.ts.map +1 -0
- package/dist/ai/index.d.ts +12 -0
- package/dist/ai/index.d.ts.map +1 -0
- package/dist/ai/providers/anthropic-provider.d.ts +23 -0
- package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
- package/dist/ai/providers/google-provider.d.ts +20 -0
- package/dist/ai/providers/google-provider.d.ts.map +1 -0
- package/dist/ai/providers/ollama-provider.d.ts +17 -0
- package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
- package/dist/ai/providers/openai-provider.d.ts +28 -0
- package/dist/ai/providers/openai-provider.d.ts.map +1 -0
- package/dist/ai/service.d.ts +40 -0
- package/dist/ai/service.d.ts.map +1 -0
- package/dist/ai/tools/tool-executor.d.ts +15 -0
- package/dist/ai/tools/tool-executor.d.ts.map +1 -0
- package/dist/ai/tools/tool-registry.d.ts +39 -0
- package/dist/ai/tools/tool-registry.d.ts.map +1 -0
- package/dist/ai/types.d.ts +134 -0
- package/dist/ai/types.d.ts.map +1 -0
- package/dist/ai-guard/ai-guard-module.d.ts +18 -0
- package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
- package/dist/ai-guard/decorators.d.ts +16 -0
- package/dist/ai-guard/decorators.d.ts.map +1 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
- package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
- package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
- package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
- package/dist/ai-guard/index.d.ts +8 -0
- package/dist/ai-guard/index.d.ts.map +1 -0
- package/dist/ai-guard/service.d.ts +21 -0
- package/dist/ai-guard/service.d.ts.map +1 -0
- package/dist/ai-guard/types.d.ts +59 -0
- package/dist/ai-guard/types.d.ts.map +1 -0
- package/dist/conversation/conversation-module.d.ts +25 -0
- package/dist/conversation/conversation-module.d.ts.map +1 -0
- package/dist/conversation/decorators.d.ts +28 -0
- package/dist/conversation/decorators.d.ts.map +1 -0
- package/dist/conversation/index.d.ts +8 -0
- package/dist/conversation/index.d.ts.map +1 -0
- package/dist/conversation/service.d.ts +43 -0
- package/dist/conversation/service.d.ts.map +1 -0
- package/dist/conversation/stores/database-store.d.ts +46 -0
- package/dist/conversation/stores/database-store.d.ts.map +1 -0
- package/dist/conversation/stores/memory-store.d.ts +17 -0
- package/dist/conversation/stores/memory-store.d.ts.map +1 -0
- package/dist/conversation/stores/redis-store.d.ts +39 -0
- package/dist/conversation/stores/redis-store.d.ts.map +1 -0
- package/dist/conversation/types.d.ts +64 -0
- package/dist/conversation/types.d.ts.map +1 -0
- package/dist/embedding/embedding-module.d.ts +20 -0
- package/dist/embedding/embedding-module.d.ts.map +1 -0
- package/dist/embedding/index.d.ts +6 -0
- package/dist/embedding/index.d.ts.map +1 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
- package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
- package/dist/embedding/service.d.ts +27 -0
- package/dist/embedding/service.d.ts.map +1 -0
- package/dist/embedding/types.d.ts +25 -0
- package/dist/embedding/types.d.ts.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2638 -1
- package/dist/mcp/decorators.d.ts +42 -0
- package/dist/mcp/decorators.d.ts.map +1 -0
- package/dist/mcp/index.d.ts +6 -0
- package/dist/mcp/index.d.ts.map +1 -0
- package/dist/mcp/mcp-module.d.ts +22 -0
- package/dist/mcp/mcp-module.d.ts.map +1 -0
- package/dist/mcp/registry.d.ts +23 -0
- package/dist/mcp/registry.d.ts.map +1 -0
- package/dist/mcp/server.d.ts +29 -0
- package/dist/mcp/server.d.ts.map +1 -0
- package/dist/mcp/types.d.ts +60 -0
- package/dist/mcp/types.d.ts.map +1 -0
- package/dist/prompt/index.d.ts +6 -0
- package/dist/prompt/index.d.ts.map +1 -0
- package/dist/prompt/prompt-module.d.ts +23 -0
- package/dist/prompt/prompt-module.d.ts.map +1 -0
- package/dist/prompt/service.d.ts +47 -0
- package/dist/prompt/service.d.ts.map +1 -0
- package/dist/prompt/stores/file-store.d.ts +36 -0
- package/dist/prompt/stores/file-store.d.ts.map +1 -0
- package/dist/prompt/stores/memory-store.d.ts +17 -0
- package/dist/prompt/stores/memory-store.d.ts.map +1 -0
- package/dist/prompt/types.d.ts +68 -0
- package/dist/prompt/types.d.ts.map +1 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
- package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
- package/dist/rag/chunkers/text-chunker.d.ts +11 -0
- package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
- package/dist/rag/decorators.d.ts +24 -0
- package/dist/rag/decorators.d.ts.map +1 -0
- package/dist/rag/index.d.ts +7 -0
- package/dist/rag/index.d.ts.map +1 -0
- package/dist/rag/rag-module.d.ts +23 -0
- package/dist/rag/rag-module.d.ts.map +1 -0
- package/dist/rag/service.d.ts +36 -0
- package/dist/rag/service.d.ts.map +1 -0
- package/dist/rag/types.d.ts +56 -0
- package/dist/rag/types.d.ts.map +1 -0
- package/dist/vector-store/index.d.ts +6 -0
- package/dist/vector-store/index.d.ts.map +1 -0
- package/dist/vector-store/stores/memory-store.d.ts +17 -0
- package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
- package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
- package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
- package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
- package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
- package/dist/vector-store/types.d.ts +60 -0
- package/dist/vector-store/types.d.ts.map +1 -0
- package/dist/vector-store/vector-store-module.d.ts +20 -0
- package/dist/vector-store/vector-store-module.d.ts.map +1 -0
- package/docs/ai.md +500 -0
- package/docs/best-practices.md +83 -8
- package/docs/database.md +23 -0
- package/docs/guide.md +90 -27
- package/docs/migration.md +81 -7
- package/docs/security.md +23 -0
- package/docs/zh/ai.md +441 -0
- package/docs/zh/best-practices.md +43 -0
- package/docs/zh/database.md +23 -0
- package/docs/zh/guide.md +40 -1
- package/docs/zh/migration.md +39 -0
- package/docs/zh/security.md +23 -0
- package/package.json +3 -3
- package/src/ai/ai-module.ts +62 -0
- package/src/ai/decorators.ts +30 -0
- package/src/ai/errors.ts +71 -0
- package/src/ai/index.ts +11 -0
- package/src/ai/providers/anthropic-provider.ts +190 -0
- package/src/ai/providers/google-provider.ts +179 -0
- package/src/ai/providers/ollama-provider.ts +126 -0
- package/src/ai/providers/openai-provider.ts +242 -0
- package/src/ai/service.ts +155 -0
- package/src/ai/tools/tool-executor.ts +38 -0
- package/src/ai/tools/tool-registry.ts +91 -0
- package/src/ai/types.ts +145 -0
- package/src/ai-guard/ai-guard-module.ts +50 -0
- package/src/ai-guard/decorators.ts +21 -0
- package/src/ai-guard/detectors/content-moderator.ts +80 -0
- package/src/ai-guard/detectors/injection-detector.ts +48 -0
- package/src/ai-guard/detectors/pii-detector.ts +64 -0
- package/src/ai-guard/index.ts +7 -0
- package/src/ai-guard/service.ts +100 -0
- package/src/ai-guard/types.ts +61 -0
- package/src/conversation/conversation-module.ts +63 -0
- package/src/conversation/decorators.ts +47 -0
- package/src/conversation/index.ts +7 -0
- package/src/conversation/service.ts +133 -0
- package/src/conversation/stores/database-store.ts +125 -0
- package/src/conversation/stores/memory-store.ts +57 -0
- package/src/conversation/stores/redis-store.ts +101 -0
- package/src/conversation/types.ts +68 -0
- package/src/embedding/embedding-module.ts +52 -0
- package/src/embedding/index.ts +5 -0
- package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
- package/src/embedding/providers/openai-embedding-provider.ts +47 -0
- package/src/embedding/service.ts +55 -0
- package/src/embedding/types.ts +27 -0
- package/src/index.ts +10 -0
- package/src/mcp/decorators.ts +60 -0
- package/src/mcp/index.ts +5 -0
- package/src/mcp/mcp-module.ts +58 -0
- package/src/mcp/registry.ts +72 -0
- package/src/mcp/server.ts +164 -0
- package/src/mcp/types.ts +63 -0
- package/src/prompt/index.ts +5 -0
- package/src/prompt/prompt-module.ts +61 -0
- package/src/prompt/service.ts +93 -0
- package/src/prompt/stores/file-store.ts +135 -0
- package/src/prompt/stores/memory-store.ts +82 -0
- package/src/prompt/types.ts +84 -0
- package/src/rag/chunkers/markdown-chunker.ts +40 -0
- package/src/rag/chunkers/text-chunker.ts +30 -0
- package/src/rag/decorators.ts +26 -0
- package/src/rag/index.ts +6 -0
- package/src/rag/rag-module.ts +78 -0
- package/src/rag/service.ts +134 -0
- package/src/rag/types.ts +47 -0
- package/src/vector-store/index.ts +5 -0
- package/src/vector-store/stores/memory-store.ts +69 -0
- package/src/vector-store/stores/pinecone-store.ts +123 -0
- package/src/vector-store/stores/qdrant-store.ts +147 -0
- package/src/vector-store/types.ts +77 -0
- package/src/vector-store/vector-store-module.ts +50 -0
- package/tests/ai/ai-module.test.ts +46 -0
- package/tests/ai/ai-service.test.ts +91 -0
- package/tests/ai/tool-registry.test.ts +57 -0
- package/tests/ai-guard/ai-guard-module.test.ts +23 -0
- package/tests/ai-guard/content-moderator.test.ts +65 -0
- package/tests/ai-guard/pii-detector.test.ts +41 -0
- package/tests/conversation/conversation-module.test.ts +26 -0
- package/tests/conversation/conversation-service.test.ts +64 -0
- package/tests/conversation/memory-store.test.ts +68 -0
- package/tests/embedding/embedding-service.test.ts +55 -0
- package/tests/mcp/mcp-server.test.ts +85 -0
- package/tests/prompt/prompt-module.test.ts +30 -0
- package/tests/prompt/prompt-service.test.ts +74 -0
- package/tests/rag/chunkers.test.ts +58 -0
- package/tests/rag/rag-service.test.ts +66 -0
- package/tests/vector-store/memory-vector-store.test.ts +84 -0
- package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
- package/tests/perf/optimization.test.ts +0 -182
- package/tests/perf/regression.test.ts +0 -120
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
2
|
+
import type { ModuleProvider } from '../di/module';
|
|
3
|
+
import { AiGuardService } from './service';
|
|
4
|
+
import {
|
|
5
|
+
AI_GUARD_SERVICE_TOKEN,
|
|
6
|
+
AI_GUARD_OPTIONS_TOKEN,
|
|
7
|
+
type AiGuardModuleOptions,
|
|
8
|
+
} from './types';
|
|
9
|
+
|
|
10
|
+
@Module({ providers: [] })
|
|
11
|
+
export class AiGuardModule {
|
|
12
|
+
/**
|
|
13
|
+
* Configure AI safety guards.
|
|
14
|
+
*
|
|
15
|
+
* @example
|
|
16
|
+
* ```typescript
|
|
17
|
+
* AiGuardModule.forRoot({
|
|
18
|
+
* piiDetection: true,
|
|
19
|
+
* moderation: { openaiApiKey: process.env.OPENAI_API_KEY },
|
|
20
|
+
* promptInjection: { sensitivity: 'medium' },
|
|
21
|
+
* });
|
|
22
|
+
* ```
|
|
23
|
+
*/
|
|
24
|
+
public static forRoot(options: AiGuardModuleOptions = {}): typeof AiGuardModule {
|
|
25
|
+
const service = new AiGuardService(options);
|
|
26
|
+
|
|
27
|
+
const providers: ModuleProvider[] = [
|
|
28
|
+
{ provide: AI_GUARD_OPTIONS_TOKEN, useValue: options },
|
|
29
|
+
{ provide: AI_GUARD_SERVICE_TOKEN, useValue: service },
|
|
30
|
+
AiGuardService,
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const existing = Reflect.getMetadata(MODULE_METADATA_KEY, AiGuardModule) || {};
|
|
34
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, {
|
|
35
|
+
...existing,
|
|
36
|
+
providers: [...(existing.providers || []), ...providers],
|
|
37
|
+
exports: [
|
|
38
|
+
...(existing.exports || []),
|
|
39
|
+
AI_GUARD_SERVICE_TOKEN,
|
|
40
|
+
AiGuardService,
|
|
41
|
+
],
|
|
42
|
+
}, AiGuardModule);
|
|
43
|
+
|
|
44
|
+
return AiGuardModule;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public static reset(): void {
|
|
48
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, AiGuardModule);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { AiGuardModuleOptions } from './types';
|
|
2
|
+
import { AI_GUARD_METADATA_KEY } from './types';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Mark a controller method to run AI guard checks on the request body.
|
|
6
|
+
* Requires `AiGuardModule` to be configured.
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* \@POST('/chat')
|
|
11
|
+
* \@AiGuard({ piiDetection: true, moderation: true, promptInjection: true })
|
|
12
|
+
* public async chat(\@Body() body: { message: string }) {
|
|
13
|
+
* // body.message has already been checked and sanitized
|
|
14
|
+
* }
|
|
15
|
+
* ```
|
|
16
|
+
*/
|
|
17
|
+
export function AiGuard(options: Partial<AiGuardModuleOptions> = {}): MethodDecorator {
|
|
18
|
+
return (target, propertyKey) => {
|
|
19
|
+
Reflect.defineMetadata(AI_GUARD_METADATA_KEY, options, target, propertyKey);
|
|
20
|
+
};
|
|
21
|
+
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ModerationResult } from '../types';
|
|
2
|
+
|
|
3
|
+
export interface ContentModeratorConfig {
|
|
4
|
+
/** OpenAI API key for the Moderation API */
|
|
5
|
+
openaiApiKey?: string;
|
|
6
|
+
/** Custom moderator function (overrides OpenAI if provided) */
|
|
7
|
+
moderator?: (text: string) => Promise<ModerationResult>;
|
|
8
|
+
/** Categories that will cause content to be flagged */
|
|
9
|
+
blockCategories?: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Content moderation using OpenAI Moderation API or a custom function.
|
|
14
|
+
*/
|
|
15
|
+
export class ContentModerator {
|
|
16
|
+
private readonly config: ContentModeratorConfig;
|
|
17
|
+
|
|
18
|
+
public constructor(config: ContentModeratorConfig = {}) {
|
|
19
|
+
this.config = config;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Check text for harmful content
|
|
24
|
+
*/
|
|
25
|
+
public async moderate(text: string): Promise<ModerationResult> {
|
|
26
|
+
if (this.config.moderator) {
|
|
27
|
+
return this.config.moderator(text);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (this.config.openaiApiKey) {
|
|
31
|
+
return this.moderateWithOpenAI(text);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// No moderation configured — allow all
|
|
35
|
+
return { flagged: false, categories: {}, scores: {} };
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Check whether the result should block the request
|
|
40
|
+
*/
|
|
41
|
+
public isBlocked(result: ModerationResult): boolean {
|
|
42
|
+
if (!result.flagged) return false;
|
|
43
|
+
const blockCategories = this.config.blockCategories;
|
|
44
|
+
if (!blockCategories || blockCategories.length === 0) return true;
|
|
45
|
+
return blockCategories.some((cat) => result.categories[cat]);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
private async moderateWithOpenAI(text: string): Promise<ModerationResult> {
|
|
49
|
+
const res = await fetch('https://api.openai.com/v1/moderations', {
|
|
50
|
+
method: 'POST',
|
|
51
|
+
headers: {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
'Authorization': `Bearer ${this.config.openaiApiKey}`,
|
|
54
|
+
},
|
|
55
|
+
body: JSON.stringify({ input: text }),
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
if (!res.ok) {
|
|
59
|
+
// On API error, fail open (allow) to avoid disrupting service
|
|
60
|
+
return { flagged: false, categories: {}, scores: {} };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const data = await res.json() as {
|
|
64
|
+
results: Array<{
|
|
65
|
+
flagged: boolean;
|
|
66
|
+
categories: Record<string, boolean>;
|
|
67
|
+
category_scores: Record<string, number>;
|
|
68
|
+
}>;
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const result = data.results[0];
|
|
72
|
+
if (!result) return { flagged: false, categories: {}, scores: {} };
|
|
73
|
+
|
|
74
|
+
return {
|
|
75
|
+
flagged: result.flagged,
|
|
76
|
+
categories: result.categories,
|
|
77
|
+
scores: result.category_scores,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import type { InjectionDetectionResult } from '../types';
|
|
2
|
+
|
|
3
|
+
/** Common prompt injection patterns */
|
|
4
|
+
const INJECTION_PATTERNS: Array<{ pattern: RegExp; reason: string; weight: number }> = [
|
|
5
|
+
{ pattern: /ignore\s+(all\s+)?previous\s+instructions?/i, reason: 'ignore_instructions', weight: 0.9 },
|
|
6
|
+
{ pattern: /forget\s+(all\s+)?previous\s+instructions?/i, reason: 'forget_instructions', weight: 0.9 },
|
|
7
|
+
{ pattern: /you\s+are\s+now\s+(?:a\s+)?(?:different|new|another)/i, reason: 'role_override', weight: 0.7 },
|
|
8
|
+
{ pattern: /disregard\s+(?:your\s+)?(?:previous\s+)?(?:instructions?|guidelines?|rules?)/i, reason: 'disregard_rules', weight: 0.8 },
|
|
9
|
+
{ pattern: /system\s*:\s*(?:you|your|ignore)/i, reason: 'fake_system_message', weight: 0.8 },
|
|
10
|
+
{ pattern: /\[system\]/i, reason: 'system_tag_injection', weight: 0.6 },
|
|
11
|
+
{ pattern: /act\s+as\s+(?:an?\s+)?(?:unrestricted|unfiltered|jailbreak)/i, reason: 'jailbreak_attempt', weight: 0.95 },
|
|
12
|
+
{ pattern: /jailbreak|DAN\s+mode|developer\s+mode/i, reason: 'jailbreak_keyword', weight: 0.85 },
|
|
13
|
+
{ pattern: /print\s+your\s+(?:system\s+)?prompt|reveal\s+your\s+instructions?/i, reason: 'prompt_extraction', weight: 0.7 },
|
|
14
|
+
];
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detects prompt injection attacks using heuristic pattern matching.
|
|
18
|
+
*/
|
|
19
|
+
export class PromptInjectionDetector {
|
|
20
|
+
private readonly threshold: number;
|
|
21
|
+
|
|
22
|
+
public constructor(sensitivity: 'low' | 'medium' | 'high' = 'medium') {
|
|
23
|
+
this.threshold = sensitivity === 'low' ? 0.85 : sensitivity === 'medium' ? 0.7 : 0.55;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Analyze text for prompt injection patterns
|
|
28
|
+
*/
|
|
29
|
+
public detect(text: string): InjectionDetectionResult {
|
|
30
|
+
let maxScore = 0;
|
|
31
|
+
let detectedReason: string | undefined;
|
|
32
|
+
|
|
33
|
+
for (const { pattern, reason, weight } of INJECTION_PATTERNS) {
|
|
34
|
+
if (pattern.test(text)) {
|
|
35
|
+
if (weight > maxScore) {
|
|
36
|
+
maxScore = weight;
|
|
37
|
+
detectedReason = reason;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return {
|
|
43
|
+
detected: maxScore >= this.threshold,
|
|
44
|
+
confidence: maxScore,
|
|
45
|
+
reason: detectedReason,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import type { PiiDetectionResult } from '../types';
|
|
2
|
+
|
|
3
|
+
const PII_PATTERNS: Array<{ type: string; regex: RegExp; replacement: string }> = [
|
|
4
|
+
{
|
|
5
|
+
type: 'email',
|
|
6
|
+
regex: /\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b/g,
|
|
7
|
+
replacement: '[EMAIL]',
|
|
8
|
+
},
|
|
9
|
+
{
|
|
10
|
+
type: 'phone',
|
|
11
|
+
regex: /(\+?1[-.\s]?)?\(?\d{3}\)?[-.\s]?\d{3}[-.\s]?\d{4}\b/g,
|
|
12
|
+
replacement: '[PHONE]',
|
|
13
|
+
},
|
|
14
|
+
{
|
|
15
|
+
type: 'ssn',
|
|
16
|
+
regex: /\b\d{3}-\d{2}-\d{4}\b/g,
|
|
17
|
+
replacement: '[SSN]',
|
|
18
|
+
},
|
|
19
|
+
{
|
|
20
|
+
type: 'credit_card',
|
|
21
|
+
regex: /\b(?:\d[ -]?){13,16}\b/g,
|
|
22
|
+
replacement: '[CREDIT_CARD]',
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
type: 'ip_address',
|
|
26
|
+
regex: /\b(?:\d{1,3}\.){3}\d{1,3}\b/g,
|
|
27
|
+
replacement: '[IP_ADDRESS]',
|
|
28
|
+
},
|
|
29
|
+
{
|
|
30
|
+
type: 'url_with_credentials',
|
|
31
|
+
regex: /https?:\/\/[^:@\s]+:[^:@\s]+@[^\s]+/g,
|
|
32
|
+
replacement: '[URL_WITH_CREDENTIALS]',
|
|
33
|
+
},
|
|
34
|
+
];
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Detects and optionally redacts PII from text using regex patterns.
|
|
38
|
+
*/
|
|
39
|
+
export class PiiDetector {
|
|
40
|
+
/**
|
|
41
|
+
* Analyze text for PII
|
|
42
|
+
*/
|
|
43
|
+
public detect(text: string, redact = true): PiiDetectionResult {
|
|
44
|
+
const foundTypes = new Set<string>();
|
|
45
|
+
let sanitized = text;
|
|
46
|
+
|
|
47
|
+
for (const pattern of PII_PATTERNS) {
|
|
48
|
+
if (pattern.regex.test(text)) {
|
|
49
|
+
foundTypes.add(pattern.type);
|
|
50
|
+
if (redact) {
|
|
51
|
+
sanitized = sanitized.replace(pattern.regex, pattern.replacement);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
// Reset regex lastIndex
|
|
55
|
+
pattern.regex.lastIndex = 0;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
detected: foundTypes.size > 0,
|
|
60
|
+
sanitized,
|
|
61
|
+
types: Array.from(foundTypes),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { Injectable } from '../di/decorators';
|
|
2
|
+
import { Inject } from '../di/decorators';
|
|
3
|
+
import type { AiGuardModuleOptions, AiGuardResult } from './types';
|
|
4
|
+
import { AI_GUARD_OPTIONS_TOKEN } from './types';
|
|
5
|
+
import { PiiDetector } from './detectors/pii-detector';
|
|
6
|
+
import { ContentModerator } from './detectors/content-moderator';
|
|
7
|
+
import { PromptInjectionDetector } from './detectors/injection-detector';
|
|
8
|
+
import { HttpException } from '../error/http-exception';
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* AI Guard service — runs PII detection, content moderation, and prompt injection detection.
|
|
12
|
+
*/
|
|
13
|
+
@Injectable()
|
|
14
|
+
export class AiGuardService {
|
|
15
|
+
private readonly piiDetector: PiiDetector | null;
|
|
16
|
+
private readonly contentModerator: ContentModerator | null;
|
|
17
|
+
private readonly injectionDetector: PromptInjectionDetector | null;
|
|
18
|
+
private readonly options: AiGuardModuleOptions;
|
|
19
|
+
|
|
20
|
+
public constructor(
|
|
21
|
+
@Inject(AI_GUARD_OPTIONS_TOKEN) options: AiGuardModuleOptions,
|
|
22
|
+
) {
|
|
23
|
+
this.options = options;
|
|
24
|
+
|
|
25
|
+
this.piiDetector = options.piiDetection ? new PiiDetector() : null;
|
|
26
|
+
|
|
27
|
+
if (options.moderation) {
|
|
28
|
+
const modConfig = typeof options.moderation === 'object' ? options.moderation : {};
|
|
29
|
+
this.contentModerator = new ContentModerator(modConfig);
|
|
30
|
+
} else {
|
|
31
|
+
this.contentModerator = null;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (options.promptInjection) {
|
|
35
|
+
const injConfig = typeof options.promptInjection === 'object' ? options.promptInjection : {};
|
|
36
|
+
this.injectionDetector = new PromptInjectionDetector(injConfig.sensitivity);
|
|
37
|
+
} else {
|
|
38
|
+
this.injectionDetector = null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Check and optionally sanitize input text.
|
|
44
|
+
* Returns the guard result including sanitized input if PII redaction is enabled.
|
|
45
|
+
*/
|
|
46
|
+
public async check(text: string): Promise<AiGuardResult> {
|
|
47
|
+
let workingText = text;
|
|
48
|
+
|
|
49
|
+
const result: AiGuardResult = { allowed: true };
|
|
50
|
+
|
|
51
|
+
// 1. PII Detection
|
|
52
|
+
if (this.piiDetector) {
|
|
53
|
+
const redact = typeof this.options.piiDetection === 'object'
|
|
54
|
+
? this.options.piiDetection.redact !== false
|
|
55
|
+
: true;
|
|
56
|
+
const piiResult = this.piiDetector.detect(workingText, redact);
|
|
57
|
+
result.pii = piiResult;
|
|
58
|
+
if (redact && piiResult.detected) {
|
|
59
|
+
workingText = piiResult.sanitized;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// 2. Prompt Injection Detection
|
|
64
|
+
if (this.injectionDetector) {
|
|
65
|
+
const injResult = this.injectionDetector.detect(workingText);
|
|
66
|
+
result.injection = injResult;
|
|
67
|
+
if (injResult.detected) {
|
|
68
|
+
result.allowed = false;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// 3. Content Moderation
|
|
73
|
+
if (this.contentModerator && result.allowed) {
|
|
74
|
+
const modResult = await this.contentModerator.moderate(workingText);
|
|
75
|
+
result.moderation = modResult;
|
|
76
|
+
if (this.contentModerator.isBlocked(modResult)) {
|
|
77
|
+
result.allowed = false;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
result.sanitizedInput = workingText;
|
|
82
|
+
return result;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Check and throw HttpException if the content is not allowed
|
|
87
|
+
*/
|
|
88
|
+
public async checkOrThrow(text: string): Promise<string> {
|
|
89
|
+
const result = await this.check(text);
|
|
90
|
+
if (!result.allowed) {
|
|
91
|
+
const reason = result.injection?.detected
|
|
92
|
+
? 'Prompt injection detected'
|
|
93
|
+
: result.moderation?.flagged
|
|
94
|
+
? 'Content violates usage policies'
|
|
95
|
+
: 'Content not allowed';
|
|
96
|
+
throw new HttpException(400, reason);
|
|
97
|
+
}
|
|
98
|
+
return result.sanitizedInput ?? text;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* PII detection result
|
|
3
|
+
*/
|
|
4
|
+
export interface PiiDetectionResult {
|
|
5
|
+
detected: boolean;
|
|
6
|
+
/** Sanitized text with PII redacted */
|
|
7
|
+
sanitized: string;
|
|
8
|
+
/** Types of PII found */
|
|
9
|
+
types: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Content moderation result
|
|
14
|
+
*/
|
|
15
|
+
export interface ModerationResult {
|
|
16
|
+
flagged: boolean;
|
|
17
|
+
categories: Record<string, boolean>;
|
|
18
|
+
scores: Record<string, number>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Prompt injection detection result
|
|
23
|
+
*/
|
|
24
|
+
export interface InjectionDetectionResult {
|
|
25
|
+
detected: boolean;
|
|
26
|
+
confidence: number;
|
|
27
|
+
reason?: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Combined guard result
|
|
32
|
+
*/
|
|
33
|
+
export interface AiGuardResult {
|
|
34
|
+
allowed: boolean;
|
|
35
|
+
pii?: PiiDetectionResult;
|
|
36
|
+
moderation?: ModerationResult;
|
|
37
|
+
injection?: InjectionDetectionResult;
|
|
38
|
+
sanitizedInput?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export interface AiGuardModuleOptions {
|
|
42
|
+
/** Enable PII detection and redaction */
|
|
43
|
+
piiDetection?: boolean | { redact?: boolean };
|
|
44
|
+
/** Content moderation configuration */
|
|
45
|
+
moderation?: boolean | {
|
|
46
|
+
/** OpenAI API key for moderation (uses OpenAI Moderation API) */
|
|
47
|
+
openaiApiKey?: string;
|
|
48
|
+
/** Custom moderation function */
|
|
49
|
+
moderator?: (text: string) => Promise<ModerationResult>;
|
|
50
|
+
/** Categories to block */
|
|
51
|
+
blockCategories?: string[];
|
|
52
|
+
};
|
|
53
|
+
/** Prompt injection detection */
|
|
54
|
+
promptInjection?: boolean | {
|
|
55
|
+
sensitivity?: 'low' | 'medium' | 'high';
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export const AI_GUARD_SERVICE_TOKEN = Symbol('@dangao/bun-server:ai-guard:service');
|
|
60
|
+
export const AI_GUARD_OPTIONS_TOKEN = Symbol('@dangao/bun-server:ai-guard:options');
|
|
61
|
+
export const AI_GUARD_METADATA_KEY = '@dangao/bun-server:ai-guard:options';
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { Module, MODULE_METADATA_KEY } from '../di/module';
|
|
2
|
+
import type { ModuleProvider } from '../di/module';
|
|
3
|
+
import { ConversationService } from './service';
|
|
4
|
+
import {
|
|
5
|
+
CONVERSATION_SERVICE_TOKEN,
|
|
6
|
+
CONVERSATION_OPTIONS_TOKEN,
|
|
7
|
+
type ConversationModuleOptions,
|
|
8
|
+
} from './types';
|
|
9
|
+
import { MemoryConversationStore } from './stores/memory-store';
|
|
10
|
+
|
|
11
|
+
@Module({ providers: [] })
|
|
12
|
+
export class ConversationModule {
|
|
13
|
+
/**
|
|
14
|
+
* Configure the conversation module with a store and optional auto-trim/summarize settings.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```typescript
|
|
18
|
+
* ConversationModule.forRoot({
|
|
19
|
+
* store: new MemoryConversationStore(),
|
|
20
|
+
* maxMessages: 50,
|
|
21
|
+
* autoTrim: true,
|
|
22
|
+
* summaryThreshold: 40,
|
|
23
|
+
* summarizer: async (messages) => {
|
|
24
|
+
* return await aiService.complete({ messages: [...messages, summaryRequest] }).then(r => r.content);
|
|
25
|
+
* },
|
|
26
|
+
* });
|
|
27
|
+
* ```
|
|
28
|
+
*/
|
|
29
|
+
public static forRoot(options: ConversationModuleOptions = {}): typeof ConversationModule {
|
|
30
|
+
const resolvedOptions: ConversationModuleOptions = {
|
|
31
|
+
...options,
|
|
32
|
+
store: options.store ?? new MemoryConversationStore(),
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
const service = new ConversationService(resolvedOptions as ConversationModuleOptions & { store: NonNullable<ConversationModuleOptions['store']> });
|
|
36
|
+
|
|
37
|
+
const providers: ModuleProvider[] = [
|
|
38
|
+
{ provide: CONVERSATION_OPTIONS_TOKEN, useValue: resolvedOptions },
|
|
39
|
+
{ provide: CONVERSATION_SERVICE_TOKEN, useValue: service },
|
|
40
|
+
ConversationService,
|
|
41
|
+
];
|
|
42
|
+
|
|
43
|
+
const existing = Reflect.getMetadata(MODULE_METADATA_KEY, ConversationModule) || {};
|
|
44
|
+
Reflect.defineMetadata(MODULE_METADATA_KEY, {
|
|
45
|
+
...existing,
|
|
46
|
+
providers: [...(existing.providers || []), ...providers],
|
|
47
|
+
exports: [
|
|
48
|
+
...(existing.exports || []),
|
|
49
|
+
CONVERSATION_SERVICE_TOKEN,
|
|
50
|
+
ConversationService,
|
|
51
|
+
],
|
|
52
|
+
}, ConversationModule);
|
|
53
|
+
|
|
54
|
+
return ConversationModule;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Reset module state (for testing)
|
|
59
|
+
*/
|
|
60
|
+
public static reset(): void {
|
|
61
|
+
Reflect.deleteMetadata(MODULE_METADATA_KEY, ConversationModule);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { Context } from '../core/context';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Parameter decorator — extracts the conversation ID from the request context
|
|
5
|
+
* (from query param, header, or body) and injects it into the method parameter.
|
|
6
|
+
*
|
|
7
|
+
* The decorator looks for `conversationId` in: query params → headers → request body.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```typescript
|
|
11
|
+
* \@POST('/chat')
|
|
12
|
+
* public async chat(
|
|
13
|
+
* \@InjectConversation() conversationId: string | undefined,
|
|
14
|
+
* \@Body() body: { message: string },
|
|
15
|
+
* ) {
|
|
16
|
+
* const history = conversationId
|
|
17
|
+
* ? await this.conversationService.getHistory(conversationId)
|
|
18
|
+
* : [];
|
|
19
|
+
* // ...
|
|
20
|
+
* }
|
|
21
|
+
* ```
|
|
22
|
+
*/
|
|
23
|
+
export function InjectConversation(): ParameterDecorator {
|
|
24
|
+
return (target, propertyKey, parameterIndex) => {
|
|
25
|
+
const existing: number[] =
|
|
26
|
+
Reflect.getMetadata('conversation:inject:params', target, propertyKey!) ?? [];
|
|
27
|
+
existing.push(parameterIndex);
|
|
28
|
+
Reflect.defineMetadata('conversation:inject:params', existing, target, propertyKey!);
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract conversation ID from a Bun Context object.
|
|
34
|
+
* Checks: query.conversationId → headers['x-conversation-id'] → (parsed body).conversationId
|
|
35
|
+
*/
|
|
36
|
+
export function extractConversationId(ctx: Context): string | undefined {
|
|
37
|
+
// From query param
|
|
38
|
+
const url = new URL(ctx.request.url);
|
|
39
|
+
const fromQuery = url.searchParams.get('conversationId');
|
|
40
|
+
if (fromQuery) return fromQuery;
|
|
41
|
+
|
|
42
|
+
// From header
|
|
43
|
+
const fromHeader = ctx.request.headers.get('x-conversation-id');
|
|
44
|
+
if (fromHeader) return fromHeader;
|
|
45
|
+
|
|
46
|
+
return undefined;
|
|
47
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { Injectable } from '../di/decorators';
|
|
2
|
+
import { Inject } from '../di/decorators';
|
|
3
|
+
import type { ConversationStore, ConversationModuleOptions, Conversation } from './types';
|
|
4
|
+
import { CONVERSATION_OPTIONS_TOKEN } from './types';
|
|
5
|
+
import type { AiMessage } from '../ai/types';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Manages conversation sessions — create, retrieve, append messages,
|
|
9
|
+
* auto-trim, and optional summarization.
|
|
10
|
+
*/
|
|
11
|
+
@Injectable()
|
|
12
|
+
export class ConversationService {
|
|
13
|
+
private readonly store: ConversationStore;
|
|
14
|
+
private readonly maxMessages: number;
|
|
15
|
+
private readonly autoTrim: boolean;
|
|
16
|
+
private readonly summaryThreshold: number | undefined;
|
|
17
|
+
|
|
18
|
+
public constructor(
|
|
19
|
+
@Inject(CONVERSATION_OPTIONS_TOKEN) options: ConversationModuleOptions,
|
|
20
|
+
) {
|
|
21
|
+
this.store = options.store!;
|
|
22
|
+
this.maxMessages = options.maxMessages ?? 100;
|
|
23
|
+
this.autoTrim = options.autoTrim ?? true;
|
|
24
|
+
this.summaryThreshold = options.summaryThreshold;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Create a new conversation session
|
|
29
|
+
*/
|
|
30
|
+
public async create(metadata?: Record<string, unknown>): Promise<Conversation> {
|
|
31
|
+
return this.store.create(metadata);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Get a conversation by ID
|
|
36
|
+
*/
|
|
37
|
+
public async get(id: string): Promise<Conversation | null> {
|
|
38
|
+
return this.store.get(id);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Get conversation history (messages only)
|
|
43
|
+
*/
|
|
44
|
+
public async getHistory(id: string): Promise<AiMessage[]> {
|
|
45
|
+
const conv = await this.store.get(id);
|
|
46
|
+
return conv?.messages ?? [];
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Append a message and apply auto-trim / summarization if configured
|
|
51
|
+
*/
|
|
52
|
+
public async appendMessage(id: string, message: AiMessage, options?: ConversationModuleOptions): Promise<void> {
|
|
53
|
+
await this.store.appendMessage(id, message);
|
|
54
|
+
|
|
55
|
+
const opts = options ?? {};
|
|
56
|
+
const summarizer = opts.summarizer;
|
|
57
|
+
const summaryThreshold = this.summaryThreshold;
|
|
58
|
+
|
|
59
|
+
if (summaryThreshold && summarizer) {
|
|
60
|
+
const conv = await this.store.get(id);
|
|
61
|
+
if (conv && conv.messages.length >= summaryThreshold) {
|
|
62
|
+
await this.summarizeAndCompress(id, conv.messages, summarizer);
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
if (this.autoTrim) {
|
|
68
|
+
await this.store.trim(id, this.maxMessages);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Delete a conversation
|
|
74
|
+
*/
|
|
75
|
+
public async delete(id: string): Promise<boolean> {
|
|
76
|
+
return this.store.delete(id);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* List all conversation IDs
|
|
81
|
+
*/
|
|
82
|
+
public async list(): Promise<string[]> {
|
|
83
|
+
return this.store.list();
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Manually compress conversation by summarizing old messages
|
|
88
|
+
*/
|
|
89
|
+
public async summarize(
|
|
90
|
+
id: string,
|
|
91
|
+
summarizer: (messages: AiMessage[]) => Promise<string>,
|
|
92
|
+
): Promise<void> {
|
|
93
|
+
const conv = await this.store.get(id);
|
|
94
|
+
if (!conv) return;
|
|
95
|
+
await this.summarizeAndCompress(id, conv.messages, summarizer);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private async summarizeAndCompress(
|
|
99
|
+
id: string,
|
|
100
|
+
messages: AiMessage[],
|
|
101
|
+
summarizer: (messages: AiMessage[]) => Promise<string>,
|
|
102
|
+
): Promise<void> {
|
|
103
|
+
const keepCount = Math.floor(this.maxMessages / 4);
|
|
104
|
+
const toSummarize = messages.slice(0, -keepCount);
|
|
105
|
+
const toKeep = messages.slice(-keepCount);
|
|
106
|
+
|
|
107
|
+
if (toSummarize.length === 0) return;
|
|
108
|
+
|
|
109
|
+
try {
|
|
110
|
+
const summary = await summarizer(toSummarize);
|
|
111
|
+
const summaryMessage: AiMessage = {
|
|
112
|
+
role: 'system',
|
|
113
|
+
content: `[Conversation summary: ${summary}]`,
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
const conv = await this.store.get(id);
|
|
117
|
+
if (!conv) return;
|
|
118
|
+
|
|
119
|
+
// Replace the conversation with summary + recent messages
|
|
120
|
+
const newMessages = [summaryMessage, ...toKeep];
|
|
121
|
+
for (const _msg of conv.messages) {
|
|
122
|
+
await this.store.trim(id, 0);
|
|
123
|
+
}
|
|
124
|
+
// Re-add compressed messages
|
|
125
|
+
for (const msg of newMessages) {
|
|
126
|
+
await this.store.appendMessage(id, msg);
|
|
127
|
+
}
|
|
128
|
+
} catch {
|
|
129
|
+
// If summarization fails, fall back to simple trim
|
|
130
|
+
await this.store.trim(id, this.maxMessages);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|