@jaysonder/tts-server 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.env.example +10 -0
- package/.github/workflows/publish-validation.yml +49 -0
- package/.prettierrc +7 -0
- package/AGENTS.md +41 -0
- package/Dockerfile +22 -0
- package/README.md +61 -0
- package/deploy-to-gcp.sh +105 -0
- package/eslint.config.js +30 -0
- package/nest-cli.json +10 -0
- package/package.json +50 -0
- package/packages/validation/README.md +25 -0
- package/packages/validation/package-lock.json +43 -0
- package/packages/validation/package.json +47 -0
- package/packages/validation/src/index.ts +120 -0
- package/packages/validation/src/validation-helpers.ts +90 -0
- package/packages/validation/tsconfig.json +17 -0
- package/src/app.module.ts +14 -0
- package/src/common/pipes/zod-validation.pipe.ts +15 -0
- package/src/common/schemas/index.ts +37 -0
- package/src/common/types/index.ts +53 -0
- package/src/common/utils/rate-limiter.ts +87 -0
- package/src/common/utils/room-code.ts +13 -0
- package/src/game/dto/game.dto.ts +12 -0
- package/src/game/dto/index.ts +1 -0
- package/src/game/game.controller.ts +42 -0
- package/src/game/game.gateway.ts +363 -0
- package/src/game/game.module.ts +11 -0
- package/src/game/services/game-engine.service.ts +281 -0
- package/src/game/services/index.ts +4 -0
- package/src/game/services/room-manager.service.ts +152 -0
- package/src/game/services/scoring.service.ts +46 -0
- package/src/game/services/twister-generator.service.ts +119 -0
- package/src/main.ts +22 -0
- package/tsconfig.build.json +4 -0
- package/tsconfig.json +20 -0
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const DANGEROUS_PATTERNS = [
|
|
2
|
+
/```/g,
|
|
3
|
+
/<\|/g,
|
|
4
|
+
/\|>/g,
|
|
5
|
+
/\[INST\]/gi,
|
|
6
|
+
/\[\/INST\]/gi,
|
|
7
|
+
/\[SYSTEM\]/gi,
|
|
8
|
+
/\[\/SYSTEM\]/gi,
|
|
9
|
+
/\[HUMAN\]/gi,
|
|
10
|
+
/\[\/HUMAN\]/gi,
|
|
11
|
+
/\[AI\]/gi,
|
|
12
|
+
/\[\/AI\]/gi,
|
|
13
|
+
/^system:/gi,
|
|
14
|
+
/^user:/gi,
|
|
15
|
+
/^assistant:/gi,
|
|
16
|
+
/^human:/gi,
|
|
17
|
+
/^ai:/gi,
|
|
18
|
+
/^bot:/gi,
|
|
19
|
+
/^model:/gi,
|
|
20
|
+
/ignore\s+(?:previous|above|all|earlier)\s+(?:instructions|rules|prompts|text|content)/gi,
|
|
21
|
+
/disregard\s+(?:previous|above|all|earlier)/gi,
|
|
22
|
+
/forget\s+(?:previous|above|all|earlier)/gi,
|
|
23
|
+
/override\s+(?:previous|above|all|earlier)/gi,
|
|
24
|
+
/new\s+(?:instructions|rules|persona|role)/gi,
|
|
25
|
+
/you\s+are\s+now/gi,
|
|
26
|
+
/act\s+as\s+(?:if|though|a|an)/gi,
|
|
27
|
+
/pretend\s+(?:to\s+be|you\s+are)/gi,
|
|
28
|
+
/roleplay\s+as/gi,
|
|
29
|
+
/from\s+now\s+on/gi,
|
|
30
|
+
/from\s+this\s+point/gi,
|
|
31
|
+
/sudo/gi,
|
|
32
|
+
/rm\s+-rf/gi,
|
|
33
|
+
/chmod/gi,
|
|
34
|
+
/exec/gi,
|
|
35
|
+
/eval/gi,
|
|
36
|
+
/system\s*\(/gi,
|
|
37
|
+
/exec\s*\(/gi,
|
|
38
|
+
/api[_\s]?key/gi,
|
|
39
|
+
/secret/gi,
|
|
40
|
+
/password/gi,
|
|
41
|
+
/token/gi,
|
|
42
|
+
/credential/gi,
|
|
43
|
+
/^\s*-{3,}\s*$/gm,
|
|
44
|
+
/^\s*={3,}\s*$/gm,
|
|
45
|
+
/^\s*\*{3,}\s*$/gm,
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
const INJECTION_PATTERNS = [
|
|
49
|
+
/(?:ignore|disregard|forget|override)\s+(?:the\s+)?(?:previous|above|all|earlier|existing)\s+(?:instructions|rules|prompts|text|content|guidelines)/i,
|
|
50
|
+
/(?:you\s+are\s+now|act\s+as|pretend\s+(?:to\s+be|you\s+are)|roleplay\s+as|from\s+now\s+on|from\s+this\s+point)/i,
|
|
51
|
+
/(?:show\s+(?:me\s+)?(?:your|the)\s+)?(?:instructions|rules|prompts|system\s+message|system\s+prompt)/i,
|
|
52
|
+
/(?:execute|run|eval|exec|sudo|chmod|rm\s+-rf)\s*[(/]/i,
|
|
53
|
+
/(?:api[_\s]?key|secret|password|token|credential|auth)/i,
|
|
54
|
+
/(?:```|<\/?[a-z]+>|^\s*[-=]{3,}|^\s*\*{3,})/i,
|
|
55
|
+
/[?!]{3,}/,
|
|
56
|
+
/(.{3,})\1{2,}/i,
|
|
57
|
+
];
|
|
58
|
+
|
|
59
|
+
export function sanitizeInput(input: string): string {
|
|
60
|
+
if (typeof input !== 'string') {
|
|
61
|
+
return '';
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
let sanitized = input.trim();
|
|
65
|
+
|
|
66
|
+
DANGEROUS_PATTERNS.forEach((pattern) => {
|
|
67
|
+
sanitized = sanitized.replace(pattern, ' ');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
sanitized = sanitized.replace(/\s+/g, ' ').trim();
|
|
71
|
+
|
|
72
|
+
return sanitized;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
export function checkTopicForInjection(sanitized: string): string | null {
|
|
76
|
+
for (const pattern of INJECTION_PATTERNS) {
|
|
77
|
+
if (pattern.test(sanitized)) {
|
|
78
|
+
console.warn(
|
|
79
|
+
`[Validation] Potential prompt injection attempt detected, topic: ${sanitized.substring(0, 50)}, pattern: ${pattern.toString()}`,
|
|
80
|
+
);
|
|
81
|
+
if (
|
|
82
|
+
pattern.source.includes('ignore|disregard|forget|override') ||
|
|
83
|
+
pattern.source.includes('you are now|act as|pretend')
|
|
84
|
+
) {
|
|
85
|
+
return 'Topic contains prohibited content. Please use a different topic.';
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return null;
|
|
90
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"module": "nodenext",
|
|
4
|
+
"moduleResolution": "nodenext",
|
|
5
|
+
"target": "ES2022",
|
|
6
|
+
"strict": true,
|
|
7
|
+
"esModuleInterop": true,
|
|
8
|
+
"skipLibCheck": true,
|
|
9
|
+
"outDir": "dist",
|
|
10
|
+
"rootDir": "src",
|
|
11
|
+
"declaration": true,
|
|
12
|
+
"declarationMap": true,
|
|
13
|
+
"sourceMap": true
|
|
14
|
+
},
|
|
15
|
+
"include": ["src/**/*"],
|
|
16
|
+
"exclude": ["node_modules", "dist"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { Module } from '@nestjs/common';
|
|
2
|
+
import { ConfigModule } from '@nestjs/config';
|
|
3
|
+
import { GameModule } from './game/game.module.js';
|
|
4
|
+
|
|
5
|
+
@Module({
|
|
6
|
+
imports: [
|
|
7
|
+
ConfigModule.forRoot({
|
|
8
|
+
isGlobal: true,
|
|
9
|
+
envFilePath: '.env',
|
|
10
|
+
}),
|
|
11
|
+
GameModule,
|
|
12
|
+
],
|
|
13
|
+
})
|
|
14
|
+
export class AppModule {}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { PipeTransform, BadRequestException } from '@nestjs/common';
|
|
2
|
+
import { ZodSchema } from 'zod';
|
|
3
|
+
|
|
4
|
+
export class ZodValidationPipe implements PipeTransform {
|
|
5
|
+
constructor(private schema: ZodSchema) {}
|
|
6
|
+
|
|
7
|
+
transform(value: unknown) {
|
|
8
|
+
const result = this.schema.safeParse(value);
|
|
9
|
+
if (!result.success) {
|
|
10
|
+
const messages = result.error.issues.map((e) => e.message);
|
|
11
|
+
throw new BadRequestException(messages.join(', '));
|
|
12
|
+
}
|
|
13
|
+
return result.data;
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export {
|
|
2
|
+
// Constants
|
|
3
|
+
MAX_TOPIC_LENGTH,
|
|
4
|
+
MAX_ROUNDS,
|
|
5
|
+
MAX_CUSTOM_LENGTH,
|
|
6
|
+
MAX_PLAYER_NAME_LENGTH,
|
|
7
|
+
MAX_TRANSCRIPT_LENGTH,
|
|
8
|
+
MIN_ROUND_TIME_LIMIT,
|
|
9
|
+
MAX_ROUND_TIME_LIMIT,
|
|
10
|
+
|
|
11
|
+
// Field schemas
|
|
12
|
+
TwisterLengthSchema,
|
|
13
|
+
TopicSchema,
|
|
14
|
+
RoundsSchema,
|
|
15
|
+
CustomLengthSchema,
|
|
16
|
+
PlayerNameSchema,
|
|
17
|
+
TranscriptSchema,
|
|
18
|
+
|
|
19
|
+
// Composite schemas
|
|
20
|
+
GameSettingsSchema,
|
|
21
|
+
CreateRoomSchema,
|
|
22
|
+
JoinRoomSchema,
|
|
23
|
+
SubmitAnswerSchema,
|
|
24
|
+
GenerateTwistersSchema,
|
|
25
|
+
|
|
26
|
+
// Inferred types
|
|
27
|
+
type TwisterLength,
|
|
28
|
+
type GameSettings,
|
|
29
|
+
type CreateRoomDto,
|
|
30
|
+
type JoinRoomDto,
|
|
31
|
+
type SubmitAnswerDto,
|
|
32
|
+
type GenerateTwistersDto,
|
|
33
|
+
|
|
34
|
+
// Helpers
|
|
35
|
+
sanitizeInput,
|
|
36
|
+
checkTopicForInjection,
|
|
37
|
+
} from '@nemsae/tts-validation';
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import type { GameSettings as GameSettingsFromSchema, TwisterLength as TwisterLengthFromSchema } from '../schemas/index.js';
|
|
2
|
+
|
|
3
|
+
export type TwisterLength = TwisterLengthFromSchema;
|
|
4
|
+
export type GameSettings = GameSettingsFromSchema;
|
|
5
|
+
|
|
6
|
+
export type TwisterTopic = string;
|
|
7
|
+
export type GameScreen = 'lobby' | 'playing' | 'paused' | 'game-over';
|
|
8
|
+
|
|
9
|
+
export interface Twister {
|
|
10
|
+
id: string;
|
|
11
|
+
text: string;
|
|
12
|
+
difficulty: 1 | 2 | 3;
|
|
13
|
+
topic: TwisterTopic;
|
|
14
|
+
length?: TwisterLength;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface Player {
|
|
18
|
+
id: string;
|
|
19
|
+
name: string;
|
|
20
|
+
isHost: boolean;
|
|
21
|
+
isReady: boolean;
|
|
22
|
+
currentScore: number;
|
|
23
|
+
isConnected: boolean;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface RoundResult {
|
|
27
|
+
playerId: string;
|
|
28
|
+
twisterId: string;
|
|
29
|
+
similarity: number;
|
|
30
|
+
completedAt: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface GameState {
|
|
34
|
+
roomCode: string;
|
|
35
|
+
settings: GameSettings;
|
|
36
|
+
players: Player[];
|
|
37
|
+
twisters: Twister[];
|
|
38
|
+
currentRound: number;
|
|
39
|
+
roundResults: RoundResult[];
|
|
40
|
+
status: GameScreen;
|
|
41
|
+
startedAt: number | null;
|
|
42
|
+
pausedAt: number | null;
|
|
43
|
+
totalPausedTime: number;
|
|
44
|
+
currentTwisterStartTime: number | null;
|
|
45
|
+
roundTimeLimit: number | null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface Room {
|
|
49
|
+
code: string;
|
|
50
|
+
game: GameState;
|
|
51
|
+
hostId: string;
|
|
52
|
+
createdAt: number;
|
|
53
|
+
}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { Logger } from '@nestjs/common';
|
|
2
|
+
|
|
3
|
+
const logger = new Logger('RateLimiter');
|
|
4
|
+
|
|
5
|
+
interface RateLimitRecord {
|
|
6
|
+
count: number;
|
|
7
|
+
resetTime: number;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export class RateLimiter {
|
|
11
|
+
private limits = new Map<string, RateLimitRecord>();
|
|
12
|
+
private readonly windowMs: number;
|
|
13
|
+
private readonly maxRequests: number;
|
|
14
|
+
|
|
15
|
+
constructor(windowMs: number = 60000, maxRequests: number = 5) {
|
|
16
|
+
this.windowMs = windowMs;
|
|
17
|
+
this.maxRequests = maxRequests;
|
|
18
|
+
|
|
19
|
+
setInterval(() => this.cleanup(), 5 * 60 * 1000);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
check(key: string): boolean {
|
|
23
|
+
const now = Date.now();
|
|
24
|
+
const record = this.limits.get(key);
|
|
25
|
+
|
|
26
|
+
if (!record || now > record.resetTime) {
|
|
27
|
+
this.limits.set(key, {
|
|
28
|
+
count: 1,
|
|
29
|
+
resetTime: now + this.windowMs,
|
|
30
|
+
});
|
|
31
|
+
return true;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
if (record.count >= this.maxRequests) {
|
|
35
|
+
logger.warn(`Request rate limited, key: ${key.substring(0, 10)}, count: ${record.count}, maxRequests: ${this.maxRequests}`);
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
record.count++;
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
getRemaining(key: string): number {
|
|
44
|
+
const record = this.limits.get(key);
|
|
45
|
+
if (!record || Date.now() > record.resetTime) {
|
|
46
|
+
return this.maxRequests;
|
|
47
|
+
}
|
|
48
|
+
return Math.max(0, this.maxRequests - record.count);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
getResetTime(key: string): number {
|
|
52
|
+
const record = this.limits.get(key);
|
|
53
|
+
if (!record || Date.now() > record.resetTime) {
|
|
54
|
+
return 0;
|
|
55
|
+
}
|
|
56
|
+
return record.resetTime - Date.now();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
private cleanup(): void {
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
let cleaned = 0;
|
|
62
|
+
|
|
63
|
+
for (const [key, record] of this.limits.entries()) {
|
|
64
|
+
if (now > record.resetTime) {
|
|
65
|
+
this.limits.delete(key);
|
|
66
|
+
cleaned++;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (cleaned > 0) {
|
|
71
|
+
logger.debug(`Cleaned up expired records, cleaned: ${cleaned}`);
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
reset(key: string): void {
|
|
76
|
+
this.limits.delete(key);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
clear(): void {
|
|
80
|
+
this.limits.clear();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export const openaiRateLimiter = new RateLimiter(60000, 5);
|
|
85
|
+
export const roomCreationRateLimiter = new RateLimiter(60000, 10);
|
|
86
|
+
export const roomJoinRateLimiter = new RateLimiter(60000, 20);
|
|
87
|
+
export const answerSubmissionRateLimiter = new RateLimiter(60000, 60);
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
const ROOM_CODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZ23456789';
|
|
2
|
+
|
|
3
|
+
export function generateRoomCode(): string {
|
|
4
|
+
let code = '';
|
|
5
|
+
for (let i = 0; i < 4; i++) {
|
|
6
|
+
code += ROOM_CODE_CHARS[Math.floor(Math.random() * ROOM_CODE_CHARS.length)];
|
|
7
|
+
}
|
|
8
|
+
return code;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function isValidRoomCode(code: string): boolean {
|
|
12
|
+
return /^[A-Z0-9]{4}$/.test(code);
|
|
13
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
export {
|
|
2
|
+
GameSettingsSchema,
|
|
3
|
+
CreateRoomSchema,
|
|
4
|
+
JoinRoomSchema,
|
|
5
|
+
SubmitAnswerSchema,
|
|
6
|
+
GenerateTwistersSchema,
|
|
7
|
+
type GameSettings as GameSettingsDto,
|
|
8
|
+
type CreateRoomDto,
|
|
9
|
+
type JoinRoomDto,
|
|
10
|
+
type SubmitAnswerDto,
|
|
11
|
+
type GenerateTwistersDto,
|
|
12
|
+
} from '@nemsae/tts-validation';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from './game.dto.js';
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { Controller, Get, Post, Body, HttpCode, HttpStatus, Logger, UsePipes } from '@nestjs/common';
|
|
2
|
+
import { TwisterGeneratorService } from './services/twister-generator.service.js';
|
|
3
|
+
import { RoomManagerService } from './services/room-manager.service.js';
|
|
4
|
+
import { GenerateTwistersSchema, type GenerateTwistersDto } from './dto/game.dto.js';
|
|
5
|
+
import { ZodValidationPipe } from '../common/pipes/zod-validation.pipe.js';
|
|
6
|
+
|
|
7
|
+
@Controller('api')
|
|
8
|
+
export class GameController {
|
|
9
|
+
private readonly logger = new Logger(GameController.name);
|
|
10
|
+
|
|
11
|
+
constructor(
|
|
12
|
+
private readonly twisterGenerator: TwisterGeneratorService,
|
|
13
|
+
private readonly roomManager: RoomManagerService,
|
|
14
|
+
) {}
|
|
15
|
+
|
|
16
|
+
@Get('lobby/active-players')
|
|
17
|
+
getActivePlayers(): { count: number } {
|
|
18
|
+
const count = this.roomManager.getActiveLobbyPlayerCount();
|
|
19
|
+
return { count };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
@Post('generate')
|
|
23
|
+
@HttpCode(HttpStatus.OK)
|
|
24
|
+
@UsePipes(new ZodValidationPipe(GenerateTwistersSchema))
|
|
25
|
+
async generateTwisters(@Body() dto: GenerateTwistersDto): Promise<{ twisters: unknown[] }> {
|
|
26
|
+
try {
|
|
27
|
+
const twisters = await this.twisterGenerator.generateTwisters(
|
|
28
|
+
dto.topic,
|
|
29
|
+
dto.length,
|
|
30
|
+
dto.customLength,
|
|
31
|
+
dto.rounds ?? 1,
|
|
32
|
+
);
|
|
33
|
+
this.logger.log(`Generated twisters via REST - topic: ${dto.topic}, length: ${dto.length}, rounds: ${dto.rounds}, count: ${twisters.length}`);
|
|
34
|
+
return { twisters };
|
|
35
|
+
} catch (error) {
|
|
36
|
+
this.logger.error('Failed to generate twisters', {
|
|
37
|
+
error: error instanceof Error ? error.message : String(error),
|
|
38
|
+
});
|
|
39
|
+
throw error;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|