@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 ADDED
@@ -0,0 +1,10 @@
1
+ OPENAI_API_KEY=your-openai-api-key-here
2
+
3
+ # GCP
4
+ PROJECT_ID=your-gcp-project-id-here
5
+ REGION=your-gcp-region-here
6
+ SERVICE_NAME=your-service-name-here
7
+ CLIENT_URL=your-client-url-here # e.g., http://localhost:5173 or https://yourdomain.com
8
+
9
+ # Port (automatically set by Cloud Run, default: 8080)
10
+ PORT=8080
@@ -0,0 +1,49 @@
1
+ name: Publish Validation Package
2
+
3
+ on:
4
+ push:
5
+ tags:
6
+ - 'validation-v*'
7
+ workflow_dispatch:
8
+
9
+ jobs:
10
+ publish:
11
+ runs-on: ubuntu-latest
12
+ permissions:
13
+ contents: read
14
+ defaults:
15
+ run:
16
+ working-directory: packages/validation
17
+ steps:
18
+ - name: Check out repository
19
+ uses: actions/checkout@v4
20
+
21
+ - name: Set up Node.js
22
+ uses: actions/setup-node@v4
23
+ with:
24
+ node-version: 20
25
+ registry-url: https://registry.npmjs.org
26
+ cache: npm
27
+ cache-dependency-path: packages/validation/package-lock.json
28
+
29
+ - name: Install package dependencies
30
+ run: npm ci
31
+
32
+ - name: Build package
33
+ run: npm run build
34
+
35
+ - name: Ensure tag matches package version
36
+ if: startsWith(github.ref, 'refs/tags/')
37
+ run: |
38
+ package_version=$(node -p "require('./package.json').version")
39
+ tag_version="${GITHUB_REF_NAME#validation-v}"
40
+
41
+ if [ "$package_version" != "$tag_version" ]; then
42
+ echo "Tag version $tag_version does not match package version $package_version"
43
+ exit 1
44
+ fi
45
+
46
+ - name: Publish package to npm
47
+ run: npm publish
48
+ env:
49
+ NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
package/.prettierrc ADDED
@@ -0,0 +1,7 @@
1
+ {
2
+ "semi": true,
3
+ "singleQuote": true,
4
+ "trailingComma": "all",
5
+ "printWidth": 120,
6
+ "tabWidth": 2
7
+ }
package/AGENTS.md ADDED
@@ -0,0 +1,41 @@
1
+ # AGENTS.md — NestJS Standards
2
+
3
+ ## Project Structure
4
+ ```
5
+ src/
6
+ ├── main.ts # Entry point
7
+ ├── app.module.ts # Root module
8
+ ├── common/
9
+ │ ├── types/index.ts # Shared types
10
+ │ └── utils/ # Logger, validation, rate-limiter, room-code
11
+ └── game/
12
+ ├── game.module.ts # Feature module
13
+ ├── game.gateway.ts # WebSocket gateway
14
+ ├── game.controller.ts # REST controller
15
+ ├── dto/ # DTOs (use class-validator)
16
+ └── services/ # GameEngine, RoomManager, TwisterGenerator, Scoring
17
+ ```
18
+
19
+ ## Conventions
20
+ - **ESM**: Use `.js` extensions in all imports
21
+ - **Decorators**: `@WebSocketGateway`, `@SubscribeMessage`, `@MessageBody`, `@Controller`, `@Get`, `@Post`
22
+ - **Validation**: class-validator decorators on DTOs; custom `validateDto()` for WebSocket payloads
23
+ - **DI**: Constructor injection with `@Injectable()` for all services
24
+ - **Naming**: PascalCase (classes), camelCase (variables/functions)
25
+ - **Error handling**: Throw `HttpException`/`BadRequestException` for HTTP errors
26
+
27
+ ## Commands
28
+ | Action | Command |
29
+ |-----------|-------------------|
30
+ | Dev | `npm run dev` |
31
+ | Build | `npm run build` |
32
+ | Start | `npm run start` |
33
+ | Lint | `npm run lint` |
34
+
35
+ ## Environment
36
+ | Variable | Required | Default |
37
+ |-----------------|----------|-------------|
38
+ | OPENAI_API_KEY | Yes | - |
39
+ | CLIENT_URL | Yes | - |
40
+ | PORT | No | 3001 |
41
+ | LOG_LEVEL | No | info |
package/Dockerfile ADDED
@@ -0,0 +1,22 @@
1
+ FROM node:20-alpine
2
+
3
+ WORKDIR /app
4
+
5
+ # Copy package files
6
+ COPY package*.json ./
7
+ COPY tsconfig.json ./
8
+
9
+ # Install all dependencies (including dev for TypeScript build)
10
+ RUN npm ci
11
+
12
+ # Copy source files
13
+ COPY src/ ./src/
14
+
15
+ # Build TypeScript
16
+ RUN npm run build
17
+
18
+ # Expose port (Cloud Run uses PORT env var)
19
+ EXPOSE 8080
20
+
21
+ # Start the server
22
+ CMD ["node", "dist/index.js"]
package/README.md ADDED
@@ -0,0 +1,61 @@
1
+ # Tone TTS Server
2
+
3
+ Multiplayer tongue twister game server. Players create/join rooms, compete to say tongue twisters, and get scored on speech accuracy.
4
+
5
+ ## Client Connections
6
+
7
+ | Type | Protocol | Purpose |
8
+ |------|----------|---------|
9
+ | **REST API** | HTTP | Generate tongue twisters, query lobby stats (`/api/*`) |
10
+ | **WebSocket** | Socket.IO | Real-time multiplayer — room management, game state, answer submission |
11
+
12
+ ### Socket.IO Events
13
+
14
+ **Client → Server:** `create-room`, `join-room`, `start-game`, `submit-answer`, `pause-game`, `resume-game`, `get-room-state`
15
+
16
+ **Server → Client:** `player-joined`, `player-left`, `game-started`, `round-advanced`, `player-submitted`, `game-paused`, `game-resumed`, `round-time-expired`, `game-ended`
17
+
18
+ ## Commands
19
+
20
+ ```bash
21
+ npm run dev # Start dev server with hot reload
22
+ npm run build # Compile TypeScript
23
+ npm start # Run compiled build
24
+ npm run lint # Lint source files
25
+ npm run lint:fix # Auto-fix lint issues
26
+ npm run format # Format with Prettier
27
+ npm run format:check # Check formatting
28
+ npm run typecheck # Type-check without emitting
29
+ npm test # Run tests
30
+ ```
31
+
32
+ ## Environment
33
+
34
+ Copy `.env.example` to `.env` and set:
35
+
36
+ - `OPENAI_API_KEY` — OpenAI API key for generating tongue twisters
37
+ - `CLIENT_URL` — Allowed CORS origin (e.g. `http://localhost:5173`)
38
+ - `PORT` — Server port (default `8080`)
39
+
40
+ ## Deploy
41
+
42
+ ```bash
43
+ ./deploy-to-gcp.sh
44
+ ```
45
+
46
+ ## Publish Validation Package
47
+
48
+ The shared validation package for the UI is published to npm as `@nemsae/tts-validation`.
49
+
50
+ Before publishing:
51
+
52
+ 1. Update the version in `packages/validation/package.json`.
53
+ 2. Refresh `packages/validation/package-lock.json` with `npm --prefix packages/validation install --package-lock-only`.
54
+ 3. Merge the version bump to `main`.
55
+ 4. Create and push a matching tag like `validation-v0.1.0`.
56
+
57
+ The `Publish Validation Package` GitHub Actions workflow will publish `packages/validation` to npm using the repo `NPM_TOKEN` secret. After that, other repos can install it with:
58
+
59
+ ```bash
60
+ npm install @nemsae/tts-validation zod
61
+ ```
@@ -0,0 +1,105 @@
1
+ #!/bin/bash
2
+
3
+ # GCP Deployment Script for TTS Server
4
+ # Replace these variables with your values
5
+ PROJECT_ID="project-id" # Replace with your GCP project ID
6
+ REGION="us-central1" # Change if you prefer a different region
7
+ SERVICE_NAME="tts-server"
8
+ CLIENT_URL="client-url" # Replace with your frontend URL
9
+
10
+ if [ -f .env ]; then
11
+ source .env
12
+ OPENAI_KEY=$OPENAI_API_KEY
13
+ REGION=$REGION
14
+ SERVICE_NAME=$SERVICE_NAME
15
+ CLIENT_URL=$CLIENT_URL
16
+ PROJECT_ID=$PROJECT_ID
17
+ else
18
+ echo -e "${YELLOW}Warning: .env file not found. Please set the required environment variables.${NC}"
19
+ fi
20
+
21
+ # Colors for output
22
+ GREEN='\033[0;32m'
23
+ YELLOW='\033[1;33m'
24
+ RED='\033[0;31m'
25
+ NC='\033[0m' # No Color
26
+
27
+ echo -e "${GREEN}Starting GCP deployment for TTS Server...${NC}"
28
+
29
+ # Check if gcloud is installed
30
+ if ! command -v gcloud &> /dev/null; then
31
+ echo -e "${RED}Error: gcloud CLI is not installed.${NC}"
32
+ echo "Please install Google Cloud SDK: https://cloud.google.com/sdk/docs/install"
33
+ exit 1
34
+ fi
35
+
36
+ # Check if user is authenticated
37
+ if ! gcloud auth list --filter=status:ACTIVE --format="value(account)" &> /dev/null; then
38
+ echo -e "${YELLOW}You need to authenticate with GCP.${NC}"
39
+ gcloud auth login
40
+ fi
41
+
42
+ # Set the project
43
+ echo -e "${GREEN}Setting project to: ${PROJECT_ID}${NC}"
44
+ gcloud config set project $PROJECT_ID
45
+
46
+ # Enable required APIs
47
+ echo -e "${GREEN}Enabling required APIs...${NC}"
48
+ gcloud services enable run.googleapis.com
49
+ gcloud services enable cloudbuild.googleapis.com
50
+ gcloud services enable secretmanager.googleapis.com
51
+
52
+ # Create secrets
53
+ echo -e "${GREEN}Creating secrets in Secret Manager...${NC}"
54
+
55
+ # Create secret for OpenAI API key
56
+ if ! gcloud secrets describe openai-api-key &> /dev/null; then
57
+ printf '%s' "$OPENAI_KEY" | gcloud secrets create openai-api-key --data-file=-
58
+ echo -e "${GREEN}Created openai-api-key secret${NC}"
59
+ else
60
+ printf '%s' "$OPENAI_KEY" | gcloud secrets versions add openai-api-key --data-file=-
61
+ echo -e "${GREEN}Updated openai-api-key secret${NC}"
62
+ fi
63
+
64
+ # Create secret for frontend URL
65
+ if ! gcloud secrets describe client-url &> /dev/null; then
66
+ printf '%s' "$CLIENT_URL" | gcloud secrets create client-url --data-file=-
67
+ echo -e "${GREEN}Created client-url secret${NC}"
68
+ else
69
+ printf '%s' "$CLIENT_URL" | gcloud secrets versions add client-url --data-file=-
70
+ echo -e "${GREEN}Updated client-url secret${NC}"
71
+ fi
72
+
73
+ # Build and push the container
74
+ echo -e "${GREEN}Building and pushing container image...${NC}"
75
+ IMAGE_TAG="gcr.io/${PROJECT_ID}/${SERVICE_NAME}:latest"
76
+ gcloud builds submit --tag $IMAGE_TAG .
77
+
78
+ # Deploy to Cloud Run
79
+ echo -e "${GREEN}Deploying to Cloud Run...${NC}"
80
+ gcloud run deploy $SERVICE_NAME \
81
+ --image=$IMAGE_TAG \
82
+ --region=$REGION \
83
+ --platform=managed \
84
+ --allow-unauthenticated \
85
+ --port=8080 \
86
+ --timeout=3600 \
87
+ --session-affinity \
88
+ --min-instances=1 \
89
+ --max-instances=10 \
90
+ --memory=512Mi \
91
+ --cpu=1 \
92
+ --set-secrets="OPENAI_API_KEY=openai-api-key:latest,CLIENT_URL=client-url:latest"
93
+
94
+ # Get the service URL
95
+ SERVICE_URL=$(gcloud run services describe $SERVICE_NAME --region=$REGION --format="value(status.url)")
96
+
97
+ echo -e "${GREEN}Deployment complete!${NC}"
98
+ echo -e "Your service is available at: ${YELLOW}${SERVICE_URL}${NC}"
99
+ echo -e "Update your frontend to connect to: ${YELLOW}${SERVICE_URL}${NC}"
100
+
101
+ # Test the deployment
102
+ echo -e "${GREEN}Testing deployment...${NC}"
103
+ curl -s -o /dev/null -w "%{http_code}" $SERVICE_URL || echo "Service might still be starting up..."
104
+
105
+ echo -e "\n${GREEN}Deployment script completed!${NC}"
@@ -0,0 +1,30 @@
1
+ import eslint from '@eslint/js';
2
+ import tseslint from 'typescript-eslint';
3
+ import eslintConfigPrettier from 'eslint-config-prettier';
4
+
5
+ export default tseslint.config(
6
+ {
7
+ ignores: ['dist/'],
8
+ },
9
+ eslint.configs.recommended,
10
+ ...tseslint.configs.recommendedTypeChecked,
11
+ {
12
+ languageOptions: {
13
+ parserOptions: {
14
+ projectService: true,
15
+ tsconfigRootDir: import.meta.dirname,
16
+ },
17
+ },
18
+ },
19
+ {
20
+ files: ['**/*.js'],
21
+ ...tseslint.configs.disableTypeChecked,
22
+ },
23
+ eslintConfigPrettier,
24
+ {
25
+ rules: {
26
+ '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }],
27
+ '@typescript-eslint/no-misused-promises': ['error', { checksVoidReturn: { attributes: false } }],
28
+ },
29
+ },
30
+ );
package/nest-cli.json ADDED
@@ -0,0 +1,10 @@
1
+ {
2
+ "$schema": "https://json.schemastore.org/nest-cli",
3
+ "collection": "@nestjs/schematics",
4
+ "sourceRoot": "src",
5
+ "compilerOptions": {
6
+ "deleteOutDir": true,
7
+ "assets": ["**/*.env"],
8
+ "watchAssets": true
9
+ }
10
+ }
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@jaysonder/tts-server",
3
+ "version": "1.0.0",
4
+ "type": "module",
5
+ "scripts": {
6
+ "dev": "nest start --watch",
7
+ "prebuild": "npm run build:validation",
8
+ "build:validation": "tsc -p packages/validation/tsconfig.json",
9
+ "publish:validation": "npm --prefix packages/validation publish --access=public",
10
+ "bump:validation:patch": "npm --prefix packages/validation version patch",
11
+ "bump:validation:minor": "npm --prefix packages/validation version minor",
12
+ "bump:validation:major": "npm --prefix packages/validation version major",
13
+ "build": "nest build",
14
+ "start": "node dist/main",
15
+ "lint": "eslint src/",
16
+ "lint:fix": "eslint src/ --fix",
17
+ "format": "prettier --write \"src/**/*.ts\"",
18
+ "format:check": "prettier --check \"src/**/*.ts\"",
19
+ "typecheck": "tsc --noEmit"
20
+ },
21
+ "dependencies": {
22
+ "@jaysonder/tts-validation": "file:packages/validation",
23
+ "@nestjs/common": "^10.4.15",
24
+ "@nestjs/config": "^3.3.0",
25
+ "@nestjs/core": "^10.4.15",
26
+ "@nestjs/platform-express": "^10.4.15",
27
+ "@nestjs/platform-socket.io": "^10.4.15",
28
+ "@nestjs/websockets": "^10.4.15",
29
+ "cors": "^2.8.6",
30
+ "dotenv": "^17.3.1",
31
+ "openai": "^6.22.0",
32
+ "reflect-metadata": "^0.2.2",
33
+ "rxjs": "^7.8.1",
34
+ "socket.io": "^4.7.0"
35
+ },
36
+ "devDependencies": {
37
+ "@eslint/js": "^9.39.4",
38
+ "@nestjs/cli": "^10.4.9",
39
+ "@nestjs/schematics": "^10.2.3",
40
+ "@types/cors": "^2.8.19",
41
+ "@types/express": "^5.0.6",
42
+ "@types/node": "^20.0.0",
43
+ "eslint": "^9.39.4",
44
+ "eslint-config-prettier": "^10.1.8",
45
+ "prettier": "^3.8.1",
46
+ "typescript": "^5.0.0",
47
+ "typescript-eslint": "^8.58.0",
48
+ "zod": "^4.3.6"
49
+ }
50
+ }
@@ -0,0 +1,25 @@
1
+ # @nemsae/tts-validation
2
+
3
+ Shared Zod schemas and inferred types for the Tone TTS server and UI.
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ npm install @nemsae/tts-validation zod
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ```ts
14
+ import { CreateRoomSchema } from '@nemsae/tts-validation';
15
+
16
+ const result = CreateRoomSchema.safeParse(formValues);
17
+
18
+ if (!result.success) {
19
+ console.log(result.error.flatten());
20
+ }
21
+ ```
22
+
23
+ ## Publishing
24
+
25
+ This package is published from the main server repo via the `Publish Validation Package` GitHub Actions workflow.
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@nemsae/tts-validation",
3
+ "version": "0.1.0",
4
+ "lockfileVersion": 3,
5
+ "requires": true,
6
+ "packages": {
7
+ "": {
8
+ "name": "@nemsae/tts-validation",
9
+ "version": "0.1.0",
10
+ "license": "MIT",
11
+ "devDependencies": {
12
+ "typescript": "^5.0.0",
13
+ "zod": "^4.3.6"
14
+ },
15
+ "peerDependencies": {
16
+ "zod": "^4.0.0"
17
+ }
18
+ },
19
+ "node_modules/typescript": {
20
+ "version": "5.9.3",
21
+ "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz",
22
+ "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
23
+ "dev": true,
24
+ "license": "Apache-2.0",
25
+ "bin": {
26
+ "tsc": "bin/tsc",
27
+ "tsserver": "bin/tsserver"
28
+ },
29
+ "engines": {
30
+ "node": ">=14.17"
31
+ }
32
+ },
33
+ "node_modules/zod": {
34
+ "version": "4.3.6",
35
+ "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz",
36
+ "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==",
37
+ "dev": true,
38
+ "funding": {
39
+ "url": "https://github.com/sponsors/colinhacks"
40
+ }
41
+ }
42
+ }
43
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@jaysonder/tts-validation",
3
+ "version": "0.1.0",
4
+ "description": "Shared Zod validation schemas for TTS",
5
+ "type": "module",
6
+ "main": "dist/index.js",
7
+ "types": "dist/index.d.ts",
8
+ "publishConfig": {
9
+ "access": "public"
10
+ },
11
+ "exports": {
12
+ ".": {
13
+ "import": "./dist/index.js",
14
+ "types": "./dist/index.d.ts"
15
+ }
16
+ },
17
+ "files": [
18
+ "dist"
19
+ ],
20
+ "scripts": {
21
+ "build": "tsc",
22
+ "prepublishOnly": "npm run build"
23
+ },
24
+ "peerDependencies": {
25
+ "zod": "^4.0.0"
26
+ },
27
+ "devDependencies": {
28
+ "typescript": "^5.0.0",
29
+ "zod": "^4.3.6"
30
+ },
31
+ "license": "MIT",
32
+ "repository": {
33
+ "type": "git",
34
+ "url": "git+https://github.com/nemsae/tts-server.git",
35
+ "directory": "packages/validation"
36
+ },
37
+ "homepage": "https://github.com/nemsae/tts-server/tree/main/packages/validation",
38
+ "bugs": {
39
+ "url": "https://github.com/nemsae/tts-server/issues"
40
+ },
41
+ "keywords": [
42
+ "tts",
43
+ "validation",
44
+ "zod",
45
+ "schemas"
46
+ ]
47
+ }
@@ -0,0 +1,120 @@
1
+ import { z } from 'zod';
2
+ import { sanitizeInput, checkTopicForInjection } from './validation-helpers.js';
3
+
4
+ // ── Constants ──────────────────────────────────────────────────────────
5
+ export const MAX_TOPIC_LENGTH = 80;
6
+ export const MAX_ROUNDS = 10;
7
+ export const MAX_CUSTOM_LENGTH = 20;
8
+ export const MAX_PLAYER_NAME_LENGTH = 20;
9
+ export const MAX_TRANSCRIPT_LENGTH = 500;
10
+ export const MIN_ROUND_TIME_LIMIT = 5;
11
+ export const MAX_ROUND_TIME_LIMIT = 120;
12
+
13
+ // ── Reusable field schemas ─────────────────────────────────────────────
14
+
15
+ export const TwisterLengthSchema = z.enum(['short', 'medium', 'long', 'custom']);
16
+
17
+ export const TopicSchema = z
18
+ .string()
19
+ .min(1, 'Topic cannot be empty')
20
+ .transform((val) => sanitizeInput(val))
21
+ .pipe(
22
+ z
23
+ .string()
24
+ .min(1, 'Topic cannot be empty after sanitization')
25
+ .max(MAX_TOPIC_LENGTH, `Topic exceeds maximum length of ${MAX_TOPIC_LENGTH} characters`)
26
+ .superRefine((val, ctx) => {
27
+ const injectionError = checkTopicForInjection(val);
28
+ if (injectionError) {
29
+ ctx.addIssue({ code: z.ZodIssueCode.custom, message: injectionError });
30
+ }
31
+ }),
32
+ );
33
+
34
+ export const RoundsSchema = z
35
+ .number()
36
+ .int('Rounds must be an integer')
37
+ .min(1, 'Rounds must be at least 1')
38
+ .max(MAX_ROUNDS, `Rounds cannot exceed ${MAX_ROUNDS}`);
39
+
40
+ export const CustomLengthSchema = z
41
+ .number()
42
+ .int('Custom length must be an integer')
43
+ .min(1, 'Custom length must be at least 1 word')
44
+ .max(MAX_CUSTOM_LENGTH, `Custom length cannot exceed ${MAX_CUSTOM_LENGTH} words`);
45
+
46
+ export const PlayerNameSchema = z
47
+ .string()
48
+ .min(1, 'Player name cannot be empty')
49
+ .transform((val) => {
50
+ let sanitized = val.replace(/<[^>]*>/g, '').trim();
51
+ sanitized = sanitized.replace(/\s+/g, ' ').trim();
52
+ return sanitized;
53
+ })
54
+ .pipe(
55
+ z
56
+ .string()
57
+ .min(1, 'Player name cannot be empty')
58
+ .max(MAX_PLAYER_NAME_LENGTH, `Player name exceeds maximum length of ${MAX_PLAYER_NAME_LENGTH} characters`),
59
+ );
60
+
61
+ export const TranscriptSchema = z
62
+ .string()
63
+ .min(1, 'Transcript cannot be empty')
64
+ .transform((val) => {
65
+ // eslint-disable-next-line no-control-regex
66
+ const controlChars = new RegExp('[\\x00-\\x1F\\x7F]', 'g');
67
+ let sanitized = val.replace(controlChars, '').replace(/\s+/g, ' ').trim();
68
+ if (sanitized.length > MAX_TRANSCRIPT_LENGTH) {
69
+ sanitized = sanitized.slice(0, MAX_TRANSCRIPT_LENGTH);
70
+ }
71
+ return sanitized;
72
+ })
73
+ .pipe(z.string().min(1, 'Transcript cannot be empty'));
74
+
75
+ // ── Composite schemas (DTOs) ──────────────────────────────────────────
76
+
77
+ export const GameSettingsSchema = z.object({
78
+ topic: TopicSchema,
79
+ length: TwisterLengthSchema,
80
+ customLength: CustomLengthSchema.optional(),
81
+ rounds: RoundsSchema,
82
+ roundTimeLimit: z.number().min(MIN_ROUND_TIME_LIMIT, `Round time limit must be at least ${MIN_ROUND_TIME_LIMIT} seconds`).max(MAX_ROUND_TIME_LIMIT, `Round time limit cannot exceed ${MAX_ROUND_TIME_LIMIT} seconds`),
83
+ autoSubmitEnabled: z.boolean().optional(),
84
+ autoSubmitDelay: z.number().optional(),
85
+ });
86
+
87
+ export const CreateRoomSchema = z.object({
88
+ playerName: PlayerNameSchema,
89
+ settings: GameSettingsSchema,
90
+ });
91
+
92
+ export const JoinRoomSchema = z.object({
93
+ roomCode: z.string().min(1, 'Room code is required'),
94
+ playerName: PlayerNameSchema,
95
+ });
96
+
97
+ export const SubmitAnswerSchema = z.object({
98
+ transcript: TranscriptSchema,
99
+ timestamp: z.number(),
100
+ });
101
+
102
+ export const GenerateTwistersSchema = z.object({
103
+ topic: TopicSchema,
104
+ length: TwisterLengthSchema,
105
+ customLength: CustomLengthSchema.optional(),
106
+ rounds: RoundsSchema.optional(),
107
+ });
108
+
109
+ // ── Inferred types ────────────────────────────────────────────────────
110
+
111
+ export type TwisterLength = z.infer<typeof TwisterLengthSchema>;
112
+ export type GameSettings = z.infer<typeof GameSettingsSchema>;
113
+ export type CreateRoomDto = z.infer<typeof CreateRoomSchema>;
114
+ export type JoinRoomDto = z.infer<typeof JoinRoomSchema>;
115
+ export type SubmitAnswerDto = z.infer<typeof SubmitAnswerSchema>;
116
+ export type GenerateTwistersDto = z.infer<typeof GenerateTwistersSchema>;
117
+
118
+ // ── Re-export helpers ─────────────────────────────────────────────────
119
+
120
+ export { sanitizeInput, checkTopicForInjection } from './validation-helpers.js';