@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
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
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
|
+
```
|
package/deploy-to-gcp.sh
ADDED
|
@@ -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}"
|
package/eslint.config.js
ADDED
|
@@ -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
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';
|