@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.
Files changed (214) hide show
  1. package/README.md +32 -0
  2. package/dist/ai/ai-module.d.ts +24 -0
  3. package/dist/ai/ai-module.d.ts.map +1 -0
  4. package/dist/ai/decorators.d.ts +25 -0
  5. package/dist/ai/decorators.d.ts.map +1 -0
  6. package/dist/ai/errors.d.ts +39 -0
  7. package/dist/ai/errors.d.ts.map +1 -0
  8. package/dist/ai/index.d.ts +12 -0
  9. package/dist/ai/index.d.ts.map +1 -0
  10. package/dist/ai/providers/anthropic-provider.d.ts +23 -0
  11. package/dist/ai/providers/anthropic-provider.d.ts.map +1 -0
  12. package/dist/ai/providers/google-provider.d.ts +20 -0
  13. package/dist/ai/providers/google-provider.d.ts.map +1 -0
  14. package/dist/ai/providers/ollama-provider.d.ts +17 -0
  15. package/dist/ai/providers/ollama-provider.d.ts.map +1 -0
  16. package/dist/ai/providers/openai-provider.d.ts +28 -0
  17. package/dist/ai/providers/openai-provider.d.ts.map +1 -0
  18. package/dist/ai/service.d.ts +40 -0
  19. package/dist/ai/service.d.ts.map +1 -0
  20. package/dist/ai/tools/tool-executor.d.ts +15 -0
  21. package/dist/ai/tools/tool-executor.d.ts.map +1 -0
  22. package/dist/ai/tools/tool-registry.d.ts +39 -0
  23. package/dist/ai/tools/tool-registry.d.ts.map +1 -0
  24. package/dist/ai/types.d.ts +134 -0
  25. package/dist/ai/types.d.ts.map +1 -0
  26. package/dist/ai-guard/ai-guard-module.d.ts +18 -0
  27. package/dist/ai-guard/ai-guard-module.d.ts.map +1 -0
  28. package/dist/ai-guard/decorators.d.ts +16 -0
  29. package/dist/ai-guard/decorators.d.ts.map +1 -0
  30. package/dist/ai-guard/detectors/content-moderator.d.ts +26 -0
  31. package/dist/ai-guard/detectors/content-moderator.d.ts.map +1 -0
  32. package/dist/ai-guard/detectors/injection-detector.d.ts +13 -0
  33. package/dist/ai-guard/detectors/injection-detector.d.ts.map +1 -0
  34. package/dist/ai-guard/detectors/pii-detector.d.ts +11 -0
  35. package/dist/ai-guard/detectors/pii-detector.d.ts.map +1 -0
  36. package/dist/ai-guard/index.d.ts +8 -0
  37. package/dist/ai-guard/index.d.ts.map +1 -0
  38. package/dist/ai-guard/service.d.ts +21 -0
  39. package/dist/ai-guard/service.d.ts.map +1 -0
  40. package/dist/ai-guard/types.d.ts +59 -0
  41. package/dist/ai-guard/types.d.ts.map +1 -0
  42. package/dist/conversation/conversation-module.d.ts +25 -0
  43. package/dist/conversation/conversation-module.d.ts.map +1 -0
  44. package/dist/conversation/decorators.d.ts +28 -0
  45. package/dist/conversation/decorators.d.ts.map +1 -0
  46. package/dist/conversation/index.d.ts +8 -0
  47. package/dist/conversation/index.d.ts.map +1 -0
  48. package/dist/conversation/service.d.ts +43 -0
  49. package/dist/conversation/service.d.ts.map +1 -0
  50. package/dist/conversation/stores/database-store.d.ts +46 -0
  51. package/dist/conversation/stores/database-store.d.ts.map +1 -0
  52. package/dist/conversation/stores/memory-store.d.ts +17 -0
  53. package/dist/conversation/stores/memory-store.d.ts.map +1 -0
  54. package/dist/conversation/stores/redis-store.d.ts +39 -0
  55. package/dist/conversation/stores/redis-store.d.ts.map +1 -0
  56. package/dist/conversation/types.d.ts +64 -0
  57. package/dist/conversation/types.d.ts.map +1 -0
  58. package/dist/embedding/embedding-module.d.ts +20 -0
  59. package/dist/embedding/embedding-module.d.ts.map +1 -0
  60. package/dist/embedding/index.d.ts +6 -0
  61. package/dist/embedding/index.d.ts.map +1 -0
  62. package/dist/embedding/providers/ollama-embedding-provider.d.ts +18 -0
  63. package/dist/embedding/providers/ollama-embedding-provider.d.ts.map +1 -0
  64. package/dist/embedding/providers/openai-embedding-provider.d.ts +18 -0
  65. package/dist/embedding/providers/openai-embedding-provider.d.ts.map +1 -0
  66. package/dist/embedding/service.d.ts +27 -0
  67. package/dist/embedding/service.d.ts.map +1 -0
  68. package/dist/embedding/types.d.ts +25 -0
  69. package/dist/embedding/types.d.ts.map +1 -0
  70. package/dist/index.d.ts +8 -0
  71. package/dist/index.d.ts.map +1 -1
  72. package/dist/index.js +2638 -1
  73. package/dist/mcp/decorators.d.ts +42 -0
  74. package/dist/mcp/decorators.d.ts.map +1 -0
  75. package/dist/mcp/index.d.ts +6 -0
  76. package/dist/mcp/index.d.ts.map +1 -0
  77. package/dist/mcp/mcp-module.d.ts +22 -0
  78. package/dist/mcp/mcp-module.d.ts.map +1 -0
  79. package/dist/mcp/registry.d.ts +23 -0
  80. package/dist/mcp/registry.d.ts.map +1 -0
  81. package/dist/mcp/server.d.ts +29 -0
  82. package/dist/mcp/server.d.ts.map +1 -0
  83. package/dist/mcp/types.d.ts +60 -0
  84. package/dist/mcp/types.d.ts.map +1 -0
  85. package/dist/prompt/index.d.ts +6 -0
  86. package/dist/prompt/index.d.ts.map +1 -0
  87. package/dist/prompt/prompt-module.d.ts +23 -0
  88. package/dist/prompt/prompt-module.d.ts.map +1 -0
  89. package/dist/prompt/service.d.ts +47 -0
  90. package/dist/prompt/service.d.ts.map +1 -0
  91. package/dist/prompt/stores/file-store.d.ts +36 -0
  92. package/dist/prompt/stores/file-store.d.ts.map +1 -0
  93. package/dist/prompt/stores/memory-store.d.ts +17 -0
  94. package/dist/prompt/stores/memory-store.d.ts.map +1 -0
  95. package/dist/prompt/types.d.ts +68 -0
  96. package/dist/prompt/types.d.ts.map +1 -0
  97. package/dist/rag/chunkers/markdown-chunker.d.ts +11 -0
  98. package/dist/rag/chunkers/markdown-chunker.d.ts.map +1 -0
  99. package/dist/rag/chunkers/text-chunker.d.ts +11 -0
  100. package/dist/rag/chunkers/text-chunker.d.ts.map +1 -0
  101. package/dist/rag/decorators.d.ts +24 -0
  102. package/dist/rag/decorators.d.ts.map +1 -0
  103. package/dist/rag/index.d.ts +7 -0
  104. package/dist/rag/index.d.ts.map +1 -0
  105. package/dist/rag/rag-module.d.ts +23 -0
  106. package/dist/rag/rag-module.d.ts.map +1 -0
  107. package/dist/rag/service.d.ts +36 -0
  108. package/dist/rag/service.d.ts.map +1 -0
  109. package/dist/rag/types.d.ts +56 -0
  110. package/dist/rag/types.d.ts.map +1 -0
  111. package/dist/vector-store/index.d.ts +6 -0
  112. package/dist/vector-store/index.d.ts.map +1 -0
  113. package/dist/vector-store/stores/memory-store.d.ts +17 -0
  114. package/dist/vector-store/stores/memory-store.d.ts.map +1 -0
  115. package/dist/vector-store/stores/pinecone-store.d.ts +27 -0
  116. package/dist/vector-store/stores/pinecone-store.d.ts.map +1 -0
  117. package/dist/vector-store/stores/qdrant-store.d.ts +29 -0
  118. package/dist/vector-store/stores/qdrant-store.d.ts.map +1 -0
  119. package/dist/vector-store/types.d.ts +60 -0
  120. package/dist/vector-store/types.d.ts.map +1 -0
  121. package/dist/vector-store/vector-store-module.d.ts +20 -0
  122. package/dist/vector-store/vector-store-module.d.ts.map +1 -0
  123. package/docs/ai.md +500 -0
  124. package/docs/best-practices.md +83 -8
  125. package/docs/database.md +23 -0
  126. package/docs/guide.md +90 -27
  127. package/docs/migration.md +81 -7
  128. package/docs/security.md +23 -0
  129. package/docs/zh/ai.md +441 -0
  130. package/docs/zh/best-practices.md +43 -0
  131. package/docs/zh/database.md +23 -0
  132. package/docs/zh/guide.md +40 -1
  133. package/docs/zh/migration.md +39 -0
  134. package/docs/zh/security.md +23 -0
  135. package/package.json +3 -3
  136. package/src/ai/ai-module.ts +62 -0
  137. package/src/ai/decorators.ts +30 -0
  138. package/src/ai/errors.ts +71 -0
  139. package/src/ai/index.ts +11 -0
  140. package/src/ai/providers/anthropic-provider.ts +190 -0
  141. package/src/ai/providers/google-provider.ts +179 -0
  142. package/src/ai/providers/ollama-provider.ts +126 -0
  143. package/src/ai/providers/openai-provider.ts +242 -0
  144. package/src/ai/service.ts +155 -0
  145. package/src/ai/tools/tool-executor.ts +38 -0
  146. package/src/ai/tools/tool-registry.ts +91 -0
  147. package/src/ai/types.ts +145 -0
  148. package/src/ai-guard/ai-guard-module.ts +50 -0
  149. package/src/ai-guard/decorators.ts +21 -0
  150. package/src/ai-guard/detectors/content-moderator.ts +80 -0
  151. package/src/ai-guard/detectors/injection-detector.ts +48 -0
  152. package/src/ai-guard/detectors/pii-detector.ts +64 -0
  153. package/src/ai-guard/index.ts +7 -0
  154. package/src/ai-guard/service.ts +100 -0
  155. package/src/ai-guard/types.ts +61 -0
  156. package/src/conversation/conversation-module.ts +63 -0
  157. package/src/conversation/decorators.ts +47 -0
  158. package/src/conversation/index.ts +7 -0
  159. package/src/conversation/service.ts +133 -0
  160. package/src/conversation/stores/database-store.ts +125 -0
  161. package/src/conversation/stores/memory-store.ts +57 -0
  162. package/src/conversation/stores/redis-store.ts +101 -0
  163. package/src/conversation/types.ts +68 -0
  164. package/src/embedding/embedding-module.ts +52 -0
  165. package/src/embedding/index.ts +5 -0
  166. package/src/embedding/providers/ollama-embedding-provider.ts +39 -0
  167. package/src/embedding/providers/openai-embedding-provider.ts +47 -0
  168. package/src/embedding/service.ts +55 -0
  169. package/src/embedding/types.ts +27 -0
  170. package/src/index.ts +10 -0
  171. package/src/mcp/decorators.ts +60 -0
  172. package/src/mcp/index.ts +5 -0
  173. package/src/mcp/mcp-module.ts +58 -0
  174. package/src/mcp/registry.ts +72 -0
  175. package/src/mcp/server.ts +164 -0
  176. package/src/mcp/types.ts +63 -0
  177. package/src/prompt/index.ts +5 -0
  178. package/src/prompt/prompt-module.ts +61 -0
  179. package/src/prompt/service.ts +93 -0
  180. package/src/prompt/stores/file-store.ts +135 -0
  181. package/src/prompt/stores/memory-store.ts +82 -0
  182. package/src/prompt/types.ts +84 -0
  183. package/src/rag/chunkers/markdown-chunker.ts +40 -0
  184. package/src/rag/chunkers/text-chunker.ts +30 -0
  185. package/src/rag/decorators.ts +26 -0
  186. package/src/rag/index.ts +6 -0
  187. package/src/rag/rag-module.ts +78 -0
  188. package/src/rag/service.ts +134 -0
  189. package/src/rag/types.ts +47 -0
  190. package/src/vector-store/index.ts +5 -0
  191. package/src/vector-store/stores/memory-store.ts +69 -0
  192. package/src/vector-store/stores/pinecone-store.ts +123 -0
  193. package/src/vector-store/stores/qdrant-store.ts +147 -0
  194. package/src/vector-store/types.ts +77 -0
  195. package/src/vector-store/vector-store-module.ts +50 -0
  196. package/tests/ai/ai-module.test.ts +46 -0
  197. package/tests/ai/ai-service.test.ts +91 -0
  198. package/tests/ai/tool-registry.test.ts +57 -0
  199. package/tests/ai-guard/ai-guard-module.test.ts +23 -0
  200. package/tests/ai-guard/content-moderator.test.ts +65 -0
  201. package/tests/ai-guard/pii-detector.test.ts +41 -0
  202. package/tests/conversation/conversation-module.test.ts +26 -0
  203. package/tests/conversation/conversation-service.test.ts +64 -0
  204. package/tests/conversation/memory-store.test.ts +68 -0
  205. package/tests/embedding/embedding-service.test.ts +55 -0
  206. package/tests/mcp/mcp-server.test.ts +85 -0
  207. package/tests/prompt/prompt-module.test.ts +30 -0
  208. package/tests/prompt/prompt-service.test.ts +74 -0
  209. package/tests/rag/chunkers.test.ts +58 -0
  210. package/tests/rag/rag-service.test.ts +66 -0
  211. package/tests/vector-store/memory-vector-store.test.ts +84 -0
  212. package/tests/interceptor/perf/interceptor-performance.test.ts +0 -340
  213. package/tests/perf/optimization.test.ts +0 -182
  214. 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,7 @@
1
+ export * from './types';
2
+ export * from './decorators';
3
+ export * from './service';
4
+ export * from './ai-guard-module';
5
+ export * from './detectors/pii-detector';
6
+ export * from './detectors/content-moderator';
7
+ export * from './detectors/injection-detector';
@@ -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,7 @@
1
+ export * from './types';
2
+ export * from './service';
3
+ export * from './decorators';
4
+ export * from './conversation-module';
5
+ export * from './stores/memory-store';
6
+ export * from './stores/redis-store';
7
+ export * from './stores/database-store';
@@ -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
+ }