First
Some checks failed
Build All Docker Images / changes (push) Has been cancelled
Build and Push App Docker Image / build (push) Has been cancelled
Build and Push Node Docker Image / build (push) Has been cancelled
Test and Lint / test-app (push) Has been cancelled
Test and Lint / test-node (push) Has been cancelled
Test and Lint / lint-dockerfiles (push) Has been cancelled
Test and Lint / security-scan (push) Has been cancelled
Build All Docker Images / build-app (push) Has been cancelled
Build All Docker Images / build-node (push) Has been cancelled
Build All Docker Images / summary (push) Has been cancelled

This commit is contained in:
hunternick87 2025-07-03 15:50:13 -04:00
commit 4169337dd0
68 changed files with 8726 additions and 0 deletions

15
node/.dockerignore Normal file
View file

@ -0,0 +1,15 @@
node_modules
npm-debug.log
dist
.git
.gitignore
README.md
.env
.env.example
.nyc_output
coverage
.vscode
logs
*.log
.DS_Store
Thumbs.db

26
node/.env.example Normal file
View file

@ -0,0 +1,26 @@
# Home Server Agent Configuration
# Server Configuration
PORT=3000
NODE_ENV=development
# Authentication (set a strong token for production)
API_TOKEN=your-secret-token-here
# Logging
LOG_LEVEL=info
# Docker Configuration
DOCKER_HOST=unix:///var/run/docker.sock
# FRP Configuration
FRPC_CONFIG_PATH=/app/data/frpc.toml
FRPC_CONTAINER_NAME=frpc
# Game Server Defaults
MINECRAFT_MEMORY=2G
VALHEIM_SERVER_NAME=My Valheim Server
VALHEIM_WORLD_NAME=MyWorld
VALHEIM_SERVER_PASS=secret123
TERRARIA_WORLD=MyWorld
TERRARIA_PASSWORD=secret123

34
node/.gitignore vendored Normal file
View file

@ -0,0 +1,34 @@
# dependencies (bun install)
node_modules
# output
out
dist
*.tgz
# code coverage
coverage
*.lcov
# logs
logs
_.log
report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# caches
.eslintcache
.cache
*.tsbuildinfo
# IntelliJ based IDEs
.idea
# Finder (MacOS) folder config
.DS_Store

380
node/DEPLOYMENT.md Normal file
View file

@ -0,0 +1,380 @@
# Home Server Agent Deployment Guide
This guide will help you deploy the Home Server Agent on your home server with Docker.
## Prerequisites
- Docker and Docker Compose installed on your home server
- At least 4GB of RAM (for running game servers)
- Open ports for the game servers you want to run
- Basic understanding of Docker and networking
## Quick Deployment
### 1. Clone and Setup
```bash
# Clone the repository
git clone <repository-url>
cd home-server-agent
# Create environment configuration
cp .env.example .env
# Edit the .env file with your settings
nano .env
```
### 2. Configure Environment
Edit `.env` file:
```bash
# IMPORTANT: Change this to a secure token
API_TOKEN=your-very-secure-token-here
# Server configuration
PORT=3000
NODE_ENV=production
LOG_LEVEL=info
# Docker socket (usually default)
DOCKER_HOST=unix:///var/run/docker.sock
# Game server settings (optional)
MINECRAFT_MEMORY=2G
VALHEIM_SERVER_NAME=Your Valheim Server
VALHEIM_WORLD_NAME=YourWorld
VALHEIM_SERVER_PASS=your-secure-password
```
### 3. Deploy with Docker Compose
```bash
# Start all services (agent + game servers)
docker-compose up -d
# Or start just the agent for testing
docker-compose -f docker-compose.dev.yml up -d
# Check status
docker-compose ps
```
## Port Configuration
The following ports will be used:
| Service | Port | Protocol | Purpose |
|---------|------|----------|---------|
| Agent API | 3000 | TCP | REST API |
| Minecraft | 25565 | TCP | Minecraft server |
| Valheim | 2456-2457 | UDP | Valheim server |
| Terraria | 7777 | TCP | Terraria server |
| Portainer | 9000 | TCP | Docker management UI |
### Firewall Configuration
**Ubuntu/Debian:**
```bash
# Allow agent API
sudo ufw allow 3000/tcp
# Allow game server ports
sudo ufw allow 25565/tcp # Minecraft
sudo ufw allow 2456:2457/udp # Valheim
sudo ufw allow 7777/tcp # Terraria
# Optional: Portainer
sudo ufw allow 9000/tcp
```
**CentOS/RHEL:**
```bash
# Allow agent API
sudo firewall-cmd --permanent --add-port=3000/tcp
# Allow game server ports
sudo firewall-cmd --permanent --add-port=25565/tcp
sudo firewall-cmd --permanent --add-port=2456-2457/udp
sudo firewall-cmd --permanent --add-port=7777/tcp
sudo firewall-cmd --reload
```
## VPS Integration
To allow your VPS to control the home server agent:
### 1. Secure the API Token
Use a strong, unique API token:
```bash
# Generate a secure token
openssl rand -hex 32
# Or use UUID
uuidgen
```
### 2. VPS Configuration
On your VPS app, configure it to make requests to your home server:
```javascript
// Example VPS integration
const HOME_SERVER_URL = 'https://your-home-server.com:3000'; // Use HTTPS in production
const API_TOKEN = 'your-secure-token-here';
// Start a game server from VPS
const startGameServer = async (serverName) => {
const response = await fetch(`${HOME_SERVER_URL}/api/gameserver/start/${serverName}`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_TOKEN}`,
'Content-Type': 'application/json'
}
});
return response.json();
};
```
### 3. Network Setup
**Option A: Port Forwarding**
- Forward port 3000 on your router to your home server
- Consider using a non-standard port for security
**Option B: VPN/Tunnel**
- Use a VPN to connect your VPS to your home network
- More secure but requires additional setup
**Option C: Reverse Proxy**
- Use nginx or Apache to proxy requests
- Can add SSL termination and additional security
## SSL/HTTPS Setup
For production use, enable HTTPS:
### Using nginx as reverse proxy:
```nginx
# /etc/nginx/sites-available/home-server-agent
server {
listen 443 ssl;
server_name your-domain.com;
ssl_certificate /path/to/your/certificate.pem;
ssl_certificate_key /path/to/your/private.key;
location / {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
```
## Monitoring and Management
### Docker Management with Portainer
```bash
# Enable Portainer
docker-compose --profile management up -d portainer
# Access at http://your-server:9000
```
### Log Management
```bash
# View agent logs
docker-compose logs -f home-server-agent
# View specific game server logs
docker-compose logs -f minecraft
# View all logs
docker-compose logs -f
```
### Health Monitoring
Set up monitoring with the included health check scripts:
```bash
# Linux health check
./health-check.sh health
# Windows health check
./health-check.ps1 -CheckType health
# Check specific game server
./health-check.sh gameserver minecraft
```
## Automatic Updates
Enable Watchtower for automatic updates:
```bash
# Enable Watchtower
docker-compose --profile management up -d watchtower
# Or manually update
docker-compose pull
docker-compose up -d
```
## Backup Strategy
### Game Data Backup
```bash
# Backup game data volumes
docker run --rm -v minecraft-data:/data -v $(pwd):/backup ubuntu tar czf /backup/minecraft-backup.tar.gz /data
# Restore game data
docker run --rm -v minecraft-data:/data -v $(pwd):/backup ubuntu tar xzf /backup/minecraft-backup.tar.gz -C /
```
### Configuration Backup
```bash
# Backup configuration
cp .env .env.backup
cp docker-compose.yml docker-compose.yml.backup
# Backup logs
tar czf logs-backup.tar.gz logs/
```
## Security Checklist
- [ ] Changed default API token
- [ ] Configured firewall rules
- [ ] Enabled SSL/HTTPS (production)
- [ ] Restricted Docker socket access
- [ ] Set up log rotation
- [ ] Configured backup strategy
- [ ] Tested health monitoring
- [ ] Documented port assignments
## Troubleshooting
### Common Issues
**1. Permission Denied (Docker Socket)**
```bash
# Add user to docker group
sudo usermod -aG docker $USER
# Or adjust socket permissions
sudo chmod 666 /var/run/docker.sock
```
**2. Port Already in Use**
```bash
# Check what's using the port
sudo netstat -tlnp | grep :3000
# Stop conflicting service
sudo systemctl stop <service-name>
```
**3. Game Server Won't Start**
```bash
# Check Docker logs
docker logs gameserver-minecraft
# Check system resources
docker system df
free -h
```
**4. API Not Responding**
```bash
# Check agent status
docker-compose ps
# Check agent logs
docker-compose logs home-server-agent
# Test local connectivity
curl http://localhost:3000/health
```
### Log Analysis
```bash
# Follow all logs
docker-compose logs -f
# Search for errors
docker-compose logs | grep -i error
# Check specific timeframe
docker-compose logs --since=1h
```
## Performance Optimization
### Resource Limits
Add resource limits to `docker-compose.yml`:
```yaml
services:
minecraft:
deploy:
resources:
limits:
memory: 2G
cpus: '2.0'
reservations:
memory: 1G
cpus: '1.0'
```
### System Requirements
**Minimum:**
- 2GB RAM
- 2 CPU cores
- 20GB storage
**Recommended:**
- 8GB RAM
- 4 CPU cores
- 100GB storage
- SSD storage for better performance
## Production Checklist
Before deploying to production:
- [ ] Secure API token configured
- [ ] Firewall rules applied
- [ ] SSL/HTTPS enabled
- [ ] Monitoring configured
- [ ] Backup strategy implemented
- [ ] Resource limits set
- [ ] Documentation updated
- [ ] Testing completed
- [ ] Network security reviewed
- [ ] Access controls verified
## Support
For issues:
1. Check the troubleshooting section
2. Review logs for error messages
3. Check Docker and system status
4. Verify network connectivity
5. Open an issue with detailed information

56
node/Dockerfile Normal file
View file

@ -0,0 +1,56 @@
# Use the official Bun image as base
FROM oven/bun:1 as builder
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
COPY bun.lockb* ./
# Install dependencies
RUN bun install --frozen-lockfile
# Copy source code
COPY src/ ./src/
COPY tsconfig.json ./
# Build the application
RUN bun run build
# Production stage
FROM node:18-alpine
# Install security updates
RUN apk update && apk upgrade && apk add --no-cache dumb-init
# Create app directory
WORKDIR /app
# Create non-root user
RUN addgroup -g 1001 -S nodejs && \
adduser -S nodeuser -u 1001
# Copy built application from builder stage
COPY --from=builder --chown=nodeuser:nodejs /app/dist ./dist
COPY --from=builder --chown=nodeuser:nodejs /app/package.json ./
# Install production dependencies
RUN npm ci --only=production && npm cache clean --force
# Create logs directory
RUN mkdir -p logs && chown nodeuser:nodejs logs
# Switch to non-root user
USER nodeuser
# Expose port
EXPOSE 3000
# Health check
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
CMD node -e "require('http').get('http://localhost:3000/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1))"
# Start the application
ENTRYPOINT ["dumb-init", "--"]
CMD ["node", "dist/index.js"]

285
node/README.md Normal file
View file

@ -0,0 +1,285 @@
# node
# Home Server Agent
A lightweight Node.js agent for managing game servers on your home server through Docker containers. This agent provides a secure REST API to start, stop, and monitor game servers remotely.
## Features
- 🎮 **Game Server Management**: Start, stop, and monitor popular game servers (Minecraft, Valheim, Terraria)
- 🐳 **Docker Integration**: Leverages Docker for containerized game server deployment
- 🔒 **Security**: Token-based authentication for API access
- 📊 **Monitoring**: Real-time status monitoring and system statistics
- 🌐 **REST API**: Clean RESTful API for remote management
- 📝 **Logging**: Comprehensive logging with Winston
- 🔄 **Auto-restart**: Containers automatically restart on failure
- 💾 **Data Persistence**: Game data persisted in Docker volumes
## Quick Start
### Prerequisites
- Docker and Docker Compose installed
- Node.js 18+ (for development)
- Bun (optional, for development)
### Development Setup
1. **Clone the repository**
```bash
git clone <repository-url>
cd home-server-agent
```
2. **Install dependencies**
```bash
bun install
# or
npm install
```
3. **Set up environment**
```bash
cp .env.example .env
# Edit .env with your configuration
```
4. **Start development server**
```bash
bun run dev
# or
npm run dev
```
### Production Deployment
1. **Build and start with Docker Compose**
```bash
docker-compose up -d
```
2. **Or start with just the agent for testing**
```bash
docker-compose -f docker-compose.dev.yml up -d
```
## API Endpoints
### Authentication
All API endpoints (except `/health`) require authentication via Bearer token or `X-API-Key` header:
```bash
# Using Bearer token
curl -H "Authorization: Bearer your-secret-token-here" http://localhost:3000/api/status
# Using API key header
curl -H "X-API-Key: your-secret-token-here" http://localhost:3000/api/status
```
### Available Endpoints
| Endpoint | Method | Description |
|----------|--------|-------------|
| `/health` | GET | Health check (no auth required) |
| `/api/status` | GET | Get overall server status |
| `/api/status/ports` | GET | Get active ports and services |
| `/api/gameserver/list` | GET | List all available game servers |
| `/api/gameserver/start/:serviceName` | POST | Start a game server |
| `/api/gameserver/stop/:serviceName` | POST | Stop a game server |
| `/api/gameserver/restart/:serviceName` | POST | Restart a game server |
| `/api/gameserver/:serviceName/status` | GET | Get specific server status |
### Example Usage
```bash
# Get server status
curl -H "Authorization: Bearer your-secret-token-here" \
http://localhost:3000/api/status
# Start Minecraft server
curl -X POST -H "Authorization: Bearer your-secret-token-here" \
http://localhost:3000/api/gameserver/start/minecraft
# Stop Minecraft server
curl -X POST -H "Authorization: Bearer your-secret-token-here" \
http://localhost:3000/api/gameserver/stop/minecraft
# Get Minecraft server status
curl -H "Authorization: Bearer your-secret-token-here" \
http://localhost:3000/api/gameserver/minecraft/status
```
## Supported Game Servers
### Minecraft
- **Image**: `itzg/minecraft-server:latest`
- **Default Port**: 25565
- **Features**: Vanilla Minecraft server with configurable settings
### Valheim
- **Image**: `lloesche/valheim-server:latest`
- **Default Ports**: 2456-2457 (UDP)
- **Features**: Dedicated Valheim server with world persistence
### Terraria
- **Image**: `ryshe/terraria:latest`
- **Default Port**: 7777
- **Features**: Terraria server with world management
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `PORT` | Server port | 3000 |
| `NODE_ENV` | Environment mode | development |
| `API_TOKEN` | Authentication token | Required |
| `LOG_LEVEL` | Logging level | info |
| `DOCKER_HOST` | Docker socket path | unix:///var/run/docker.sock |
### Game Server Configuration
Game servers can be configured by modifying the environment variables in `docker-compose.yml`:
```yaml
minecraft:
environment:
EULA: "TRUE"
TYPE: "VANILLA"
MEMORY: "2G"
DIFFICULTY: "normal"
MAX_PLAYERS: "20"
```
## Security
- **Authentication**: All API endpoints require token authentication
- **Docker Security**: Containers run as non-root users where possible
- **Network Isolation**: Services run in isolated Docker networks
- **Read-only Docker Socket**: Docker socket is mounted read-only for security
## Monitoring
The agent provides comprehensive monitoring capabilities:
- **System Stats**: Docker version, container counts, resource usage
- **Port Monitoring**: Active ports and their associated services
- **Container Status**: Real-time container health and uptime
- **Logs**: Structured logging with Winston
## Development
### Project Structure
```
src/
├── index.ts # Main application entry point
├── middleware/
│ └── auth.ts # Authentication middleware
├── routes/
│ ├── status.ts # Status and monitoring routes
│ └── gameServer.ts # Game server management routes
├── services/
│ └── dockerManager.ts # Docker container management
└── utils/
└── logger.ts # Logging configuration
```
### Scripts
```bash
# Development
bun run dev # Start development server with hot reload
bun run build # Build the application
bun run start # Start production server
# Docker
bun run docker:build # Build Docker image
bun run docker:run # Run with Docker Compose
```
## Docker Volumes
The following volumes are used for data persistence:
- `minecraft-data`: Minecraft world data and configuration
- `valheim-data`: Valheim world data and configuration
- `terraria-data`: Terraria world data and configuration
- `./logs`: Application logs (mounted from host)
## Optional Services
The Docker Compose file includes optional management services:
### Portainer (Docker Management UI)
```bash
docker-compose --profile management up -d portainer
```
Access at: http://localhost:9000
### Watchtower (Auto-updates)
```bash
docker-compose --profile management up -d watchtower
```
## Troubleshooting
### Common Issues
1. **Permission Denied (Docker Socket)**
```bash
# Ensure Docker socket has proper permissions
sudo chmod 666 /var/run/docker.sock
```
2. **Port Already in Use**
```bash
# Check what's using the port
netstat -tlnp | grep :3000
```
3. **Container Start Failures**
```bash
# Check container logs
docker logs gameserver-minecraft
```
### Logs
Application logs are available in:
- `./logs/combined.log` - All logs
- `./logs/error.log` - Error logs only
- Console output (development mode)
## License
This project is licensed under the MIT License.
## Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Add tests if applicable
5. Submit a pull request
## Support
For issues and questions:
1. Check the troubleshooting section
2. Review the logs for error messages
3. Open an issue on GitHub with detailed informationall dependencies:
```bash
bun install
```
To run:
```bash
bun run index.ts
```
This project was created using `bun init` in bun v1.2.6. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime.

430
node/bun.lock Normal file
View file

@ -0,0 +1,430 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "node",
"dependencies": {
"cors": "^2.8.5",
"dockerode": "^4.0.2",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"winston": "^3.11.0",
},
"devDependencies": {
"@types/bun": "latest",
"@types/cors": "^2.8.17",
"@types/express": "^4.17.21",
"@types/morgan": "^1.9.9",
"@types/node": "^20.10.0",
},
"peerDependencies": {
"typescript": "^5",
},
},
},
"packages": {
"@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="],
"@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="],
"@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="],
"@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="],
"@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="],
"@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="],
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="],
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="],
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="],
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
"@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="],
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="],
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="],
"@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="],
"@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="],
"@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="],
"@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="],
"@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="],
"@types/morgan": ["@types/morgan@1.9.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA=="],
"@types/node": ["@types/node@20.19.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA=="],
"@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="],
"@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="],
"@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="],
"@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="],
"@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="],
"accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="],
"asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="],
"bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="],
"bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="],
"body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="],
"bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="],
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="],
"chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="],
"color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="],
"color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="],
"color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="],
"color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="],
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
"content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="],
"cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="],
"debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="],
"dockerode": ["dockerode@4.0.7", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA=="],
"dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="],
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="],
"ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="],
"emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="],
"encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="],
"etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="],
"express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="],
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="],
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="],
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"helmet": ["helmet@7.2.0", "", {}, "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw=="],
"http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="],
"iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="],
"is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="],
"logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="],
"merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="],
"methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="],
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"morgan": ["morgan@1.10.0", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.0.2" } }, "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ=="],
"ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
"nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="],
"negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="],
"object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="],
"object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="],
"on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="],
"on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="],
"once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="],
"one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="],
"protobufjs": ["protobufjs@7.5.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw=="],
"proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="],
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
"qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="],
"readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="],
"require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="],
"serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="],
"side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="],
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="],
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
"split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="],
"ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="],
"strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="],
"tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="],
"text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
"tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="],
"type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="],
"uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="],
"winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="],
"wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="],
"yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="],
"yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="],
"@types/body-parser/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@types/connect/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@types/cors/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@types/express-serve-static-core/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@types/morgan/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@types/send/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"@types/serve-static/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="],
"basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="],
"bun-types/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"docker-modem/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"logform/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="],
"protobufjs/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="],
"send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
"send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"@types/body-parser/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/connect/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/cors/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/morgan/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/send/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"@types/serve-static/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"bun-types/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
"docker-modem/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"protobufjs/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="],
}
}

View file

@ -0,0 +1,49 @@
version: '3.8'
services:
# Home Server Agent (Development)
home-server-agent:
build: .
container_name: home-server-agent-dev
ports:
- "3000:3000"
environment:
- NODE_ENV=development
- API_TOKEN=dev-token-123
- LOG_LEVEL=debug
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./logs:/app/logs
- ./src:/app/src
networks:
- game-network
restart: unless-stopped
# Single Minecraft Server for Testing
minecraft:
image: itzg/minecraft-server:latest
container_name: gameserver-minecraft
ports:
- "25565:25565"
environment:
EULA: "TRUE"
TYPE: "VANILLA"
MEMORY: "1G"
DIFFICULTY: "peaceful"
MAX_PLAYERS: "5"
ONLINE_MODE: "false"
volumes:
- minecraft-dev-data:/data
networks:
- game-network
restart: unless-stopped
labels:
- "game-server=minecraft"
volumes:
minecraft-dev-data:
driver: local
networks:
game-network:
driver: bridge

153
node/docker-compose.yml Normal file
View file

@ -0,0 +1,153 @@
version: '3.8'
services:
# Home Server Agent
home-server-agent:
build: .
container_name: home-server-agent
ports:
- "3000:3000"
environment:
- NODE_ENV=production
- API_TOKEN=${API_TOKEN:-your-secret-token-here}
- LOG_LEVEL=info
- FRPC_CONFIG_PATH=${FRPC_CONFIG_PATH:-/app/data/frpc.toml}
- FRPC_CONTAINER_NAME=${FRPC_CONTAINER_NAME:-frpc}
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- ./logs:/app/logs
- ./data:/app/data
networks:
- game-network
restart: unless-stopped
depends_on:
- minecraft
- valheim
- terraria
# Minecraft Server
minecraft:
image: itzg/minecraft-server:latest
container_name: gameserver-minecraft
ports:
- "25565:25565"
environment:
EULA: "TRUE"
TYPE: "VANILLA"
MEMORY: "2G"
DIFFICULTY: "normal"
SPAWN_PROTECTION: "0"
MAX_PLAYERS: "20"
ONLINE_MODE: "false"
ALLOW_NETHER: "true"
ANNOUNCE_PLAYER_ACHIEVEMENTS: "true"
ENABLE_COMMAND_BLOCK: "true"
FORCE_GAMEMODE: "false"
GENERATE_STRUCTURES: "true"
HARDCORE: "false"
MAX_BUILD_HEIGHT: "256"
MAX_TICK_TIME: "60000"
SPAWN_ANIMALS: "true"
SPAWN_MONSTERS: "true"
SPAWN_NPCS: "true"
VIEW_DISTANCE: "10"
volumes:
- minecraft-data:/data
networks:
- game-network
restart: unless-stopped
labels:
- "game-server=minecraft"
# Valheim Server
valheim:
image: lloesche/valheim-server:latest
container_name: gameserver-valheim
ports:
- "2456:2456/udp"
- "2457:2457/udp"
environment:
SERVER_NAME: "My Valheim Server"
WORLD_NAME: "MyWorld"
SERVER_PASS: "secret123"
SERVER_PUBLIC: "false"
ADMINLIST_IDS: ""
BANNEDLIST_IDS: ""
PERMITTEDLIST_IDS: ""
volumes:
- valheim-data:/config
networks:
- game-network
restart: unless-stopped
labels:
- "game-server=valheim"
# Terraria Server
terraria:
image: ryshe/terraria:latest
container_name: gameserver-terraria
ports:
- "7777:7777"
environment:
WORLD: "MyWorld"
PASSWORD: "secret123"
MAXPLAYERS: "16"
DIFFICULTY: "1"
AUTOCREATE: "2"
BANLIST: ""
SECURE: "1"
LANGUAGE: "en-US"
volumes:
- terraria-data:/world
networks:
- game-network
restart: unless-stopped
labels:
- "game-server=terraria"
# Optional: Portainer for Docker management
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
ports:
- "9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
- portainer-data:/data
networks:
- game-network
restart: unless-stopped
profiles:
- management
# Optional: Watchtower for automatic updates
watchtower:
image: containrrr/watchtower:latest
container_name: watchtower
volumes:
- /var/run/docker.sock:/var/run/docker.sock:ro
environment:
- WATCHTOWER_CLEANUP=true
- WATCHTOWER_SCHEDULE=0 0 2 * * * # 2 AM daily
networks:
- game-network
restart: unless-stopped
profiles:
- management
volumes:
minecraft-data:
driver: local
valheim-data:
driver: local
terraria-data:
driver: local
portainer-data:
driver: local
networks:
game-network:
driver: bridge
ipam:
config:
- subnet: 172.20.0.0/16

110
node/health-check.ps1 Normal file
View file

@ -0,0 +1,110 @@
# Health check script for the Home Server Agent (PowerShell)
# This script can be used with monitoring tools on Windows
param(
[Parameter(Mandatory=$true)]
[ValidateSet('health', 'gameserver')]
[string]$CheckType,
[Parameter(Mandatory=$false)]
[string]$ServerName,
[Parameter(Mandatory=$false)]
[string]$AgentUrl = "http://localhost:3000",
[Parameter(Mandatory=$false)]
[string]$ApiToken = $env:API_TOKEN,
[Parameter(Mandatory=$false)]
[int]$Timeout = 10
)
# Function to check if agent is healthy
function Test-AgentHealth {
try {
$headers = @{}
if ($ApiToken) {
$headers["Authorization"] = "Bearer $ApiToken"
}
$response = Invoke-RestMethod -Uri "$AgentUrl/health" -Headers $headers -TimeoutSec $Timeout -ErrorAction Stop
if ($response.status -eq "healthy") {
Write-Output "OK: Home Server Agent is healthy"
return 0
} else {
Write-Output "CRITICAL: Home Server Agent reported unhealthy status"
return 2
}
}
catch {
Write-Output "CRITICAL: Home Server Agent is not responding - $($_.Exception.Message)"
return 2
}
}
# Function to check game server status
function Test-GameServerStatus {
param([string]$ServerName)
if (-not $ServerName) {
Write-Output "UNKNOWN: Server name not provided"
return 3
}
if (-not $ApiToken) {
Write-Output "UNKNOWN: API_TOKEN not provided"
return 3
}
try {
$headers = @{
"Authorization" = "Bearer $ApiToken"
}
$response = Invoke-RestMethod -Uri "$AgentUrl/api/gameserver/$ServerName/status" -Headers $headers -TimeoutSec $Timeout -ErrorAction Stop
switch ($response.status) {
"running" {
Write-Output "OK: $ServerName is running"
return 0
}
"stopped" {
Write-Output "WARNING: $ServerName is stopped"
return 1
}
default {
Write-Output "CRITICAL: $ServerName status unknown ($($response.status))"
return 2
}
}
}
catch {
Write-Output "CRITICAL: Cannot check $ServerName status - $($_.Exception.Message)"
return 2
}
}
# Main execution
$exitCode = 0
switch ($CheckType) {
"health" {
$exitCode = Test-AgentHealth
}
"gameserver" {
$exitCode = Test-GameServerStatus -ServerName $ServerName
}
default {
Write-Output "Usage: health-check.ps1 -CheckType {health|gameserver} [-ServerName <name>]"
Write-Output "Environment variables:"
Write-Output " API_TOKEN - Authentication token for API access"
Write-Output "Parameters:"
Write-Output " -AgentUrl - URL of the Home Server Agent (default: http://localhost:3000)"
Write-Output " -ApiToken - Authentication token for API access"
Write-Output " -Timeout - Request timeout in seconds (default: 10)"
$exitCode = 3
}
}
exit $exitCode

98
node/health-check.sh Normal file
View file

@ -0,0 +1,98 @@
#!/bin/bash
# Health check script for the Home Server Agent
# This script can be used with monitoring tools like Nagios, Zabbix, etc.
AGENT_URL="${AGENT_URL:-http://localhost:3000}"
API_TOKEN="${API_TOKEN:-}"
TIMEOUT="${TIMEOUT:-10}"
# Function to check if agent is healthy
check_health() {
local response
local status_code
if [ -n "$API_TOKEN" ]; then
response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \
-H "Authorization: Bearer $API_TOKEN" \
"$AGENT_URL/health" 2>/dev/null)
else
response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \
"$AGENT_URL/health" 2>/dev/null)
fi
status_code="${response: -3}"
if [ "$status_code" = "200" ]; then
echo "OK: Home Server Agent is healthy"
return 0
else
echo "CRITICAL: Home Server Agent is not responding (HTTP $status_code)"
return 2
fi
}
# Function to check game server status
check_gameserver() {
local server_name="$1"
local response
local status_code
if [ -z "$server_name" ]; then
echo "UNKNOWN: Server name not provided"
return 3
fi
if [ -z "$API_TOKEN" ]; then
echo "UNKNOWN: API_TOKEN not provided"
return 3
fi
response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \
-H "Authorization: Bearer $API_TOKEN" \
"$AGENT_URL/api/gameserver/$server_name/status" 2>/dev/null)
status_code="${response: -3}"
response_body="${response%???}"
if [ "$status_code" = "200" ]; then
# Parse JSON response to get status
status=$(echo "$response_body" | grep -o '"status":"[^"]*"' | cut -d'"' -f4)
case "$status" in
"running")
echo "OK: $server_name is running"
return 0
;;
"stopped")
echo "WARNING: $server_name is stopped"
return 1
;;
*)
echo "CRITICAL: $server_name status unknown ($status)"
return 2
;;
esac
else
echo "CRITICAL: Cannot check $server_name status (HTTP $status_code)"
return 2
fi
}
# Main execution
case "$1" in
"health")
check_health
;;
"gameserver")
check_gameserver "$2"
;;
*)
echo "Usage: $0 {health|gameserver <server_name>}"
echo "Environment variables:"
echo " AGENT_URL - URL of the Home Server Agent (default: http://localhost:3000)"
echo " API_TOKEN - Authentication token for API access"
echo " TIMEOUT - Request timeout in seconds (default: 10)"
exit 3
;;
esac

View file

@ -0,0 +1,23 @@
[Unit]
Description=Home Server Agent
Documentation=https://github.com/your-repo/home-server-agent
Requires=docker.service
After=docker.service
Wants=network-online.target
After=network-online.target
[Service]
Type=forking
RemainAfterExit=yes
WorkingDirectory=/opt/home-server-agent
ExecStart=/usr/local/bin/docker-compose up -d
ExecStop=/usr/local/bin/docker-compose down
ExecReload=/usr/local/bin/docker-compose restart
TimeoutStartSec=0
Restart=on-failure
RestartSec=10
User=docker
Group=docker
[Install]
WantedBy=multi-user.target

View file

@ -0,0 +1,54 @@
# Windows Service Installation Script
# Run as Administrator
$serviceName = "HomeServerAgent"
$serviceDisplayName = "Home Server Agent"
$serviceDescription = "Lightweight agent for managing game servers"
$servicePath = "C:\Program Files\HomeServerAgent"
$dockerComposePath = "$servicePath\docker-compose.yml"
# Check if running as administrator
if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) {
Write-Warning "Please run this script as Administrator"
exit 1
}
# Create service directory
if (!(Test-Path $servicePath)) {
New-Item -ItemType Directory -Path $servicePath -Force
}
# Copy files to service directory
Copy-Item -Path ".\*" -Destination $servicePath -Recurse -Force
# Install NSSM (Non-Sucking Service Manager) if not present
$nssmPath = "$servicePath\nssm.exe"
if (!(Test-Path $nssmPath)) {
Write-Host "Downloading NSSM..."
Invoke-WebRequest -Uri "https://nssm.cc/release/nssm-2.24.zip" -OutFile "$servicePath\nssm.zip"
Expand-Archive -Path "$servicePath\nssm.zip" -DestinationPath $servicePath
Copy-Item -Path "$servicePath\nssm-2.24\win64\nssm.exe" -Destination $nssmPath
Remove-Item -Path "$servicePath\nssm.zip" -Force
Remove-Item -Path "$servicePath\nssm-2.24" -Recurse -Force
}
# Create service
& $nssmPath install $serviceName "docker-compose"
& $nssmPath set $serviceName AppDirectory $servicePath
& $nssmPath set $serviceName AppParameters "up -d"
& $nssmPath set $serviceName DisplayName $serviceDisplayName
& $nssmPath set $serviceName Description $serviceDescription
& $nssmPath set $serviceName Start SERVICE_AUTO_START
& $nssmPath set $serviceName AppStopMethodConsole 30000
& $nssmPath set $serviceName AppStopMethodWindow 30000
& $nssmPath set $serviceName AppStopMethodThreads 30000
& $nssmPath set $serviceName AppKillProcessTree 1
# Set service to restart on failure
& $nssmPath set $serviceName AppRestartDelay 10000
& $nssmPath set $serviceName AppNoConsole 1
Write-Host "Service installed successfully!"
Write-Host "To start the service: Start-Service $serviceName"
Write-Host "To stop the service: Stop-Service $serviceName"
Write-Host "To remove the service: & '$nssmPath' remove $serviceName confirm"

34
node/package.json Normal file
View file

@ -0,0 +1,34 @@
{
"name": "home-server-agent",
"version": "1.0.0",
"description": "Lightweight agent for managing game servers on home server",
"main": "dist/index.js",
"scripts": {
"dev": "bun run --watch src/index.ts",
"build": "bun build src/index.ts --outdir dist --target node",
"start": "node dist/index.js",
"docker:build": "docker build -t home-server-agent .",
"docker:run": "docker-compose up -d"
},
"type": "module",
"private": true,
"dependencies": {
"express": "^4.18.2",
"cors": "^2.8.5",
"helmet": "^7.1.0",
"morgan": "^1.10.0",
"winston": "^3.11.0",
"dockerode": "^4.0.2",
"dotenv": "^16.3.1"
},
"devDependencies": {
"@types/bun": "latest",
"@types/express": "^4.17.21",
"@types/cors": "^2.8.17",
"@types/morgan": "^1.9.9",
"@types/node": "^20.10.0"
},
"peerDependencies": {
"typescript": "^5"
}
}

83
node/src/index.ts Normal file
View file

@ -0,0 +1,83 @@
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';
import dotenv from 'dotenv';
import { logger } from './utils/logger.js';
import { authMiddleware } from './middleware/auth.js';
import { statusRouter } from './routes/status.js';
import { gameServerRouter } from './routes/gameServer.js';
import { frpcRouter } from './routes/frpc.js';
import { DockerManager } from './services/dockerManager.js';
// Load environment variables
dotenv.config();
const app = express();
const PORT = process.env.PORT || 3000;
// Initialize Docker manager
const dockerManager = new DockerManager();
// Middleware
app.use(helmet());
app.use(cors());
app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } }));
app.use(express.json());
// Authentication middleware for protected routes
app.use('/api', authMiddleware);
// Routes
app.use('/api/status', statusRouter);
app.use('/api/gameserver', gameServerRouter);
app.use('/api/frpc', frpcRouter);
// Health check endpoint (no auth required)
app.get('/health', (req, res) => {
res.json({ status: 'healthy', timestamp: new Date().toISOString() });
});
// Root endpoint
app.get('/', (req, res) => {
res.json({
name: 'Home Server Agent',
version: '1.0.0',
description: 'Lightweight agent for managing game servers',
endpoints: {
'/health': 'Health check (no auth)',
'/api/status': 'Get server status',
'/api/gameserver/list': 'List game servers',
'/api/gameserver/start/:serviceName': 'Start a game server',
'/api/gameserver/stop/:serviceName': 'Stop a game server',
'/api/gameserver/:serviceName/status': 'Get specific server status',
'/api/frpc/status': 'Get frpc status',
'/api/frpc/update-config': 'Update frpc configuration',
'/api/frpc/restart': 'Restart frpc container',
'/api/frpc/push-and-restart': 'Update config and restart frpc'
}
});
});
// Error handling middleware
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error(`Error: ${err.message}`, { stack: err.stack });
res.status(500).json({ error: 'Internal server error' });
});
// Start server
app.listen(PORT, () => {
logger.info(`Home Server Agent listening on port ${PORT}`);
logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`);
});
// Graceful shutdown
process.on('SIGTERM', () => {
logger.info('Received SIGTERM, shutting down gracefully');
process.exit(0);
});
process.on('SIGINT', () => {
logger.info('Received SIGINT, shutting down gracefully');
process.exit(0);
});

View file

@ -0,0 +1,25 @@
import { Request, Response, NextFunction } from 'express';
import { logger } from '../utils/logger.js';
export const authMiddleware = (req: Request, res: Response, next: NextFunction) => {
const token = req.headers.authorization?.replace('Bearer ', '') || req.headers['x-api-key'];
const expectedToken = process.env.API_TOKEN;
if (!expectedToken) {
logger.warn('No API_TOKEN configured, skipping authentication');
return next();
}
if (!token) {
logger.warn(`Unauthorized request from ${req.ip} - no token provided`);
return res.status(401).json({ error: 'Authentication required' });
}
if (token !== expectedToken) {
logger.warn(`Unauthorized request from ${req.ip} - invalid token`);
return res.status(403).json({ error: 'Invalid token' });
}
logger.info(`Authenticated request from ${req.ip}`);
next();
};

158
node/src/routes/frpc.ts Normal file
View file

@ -0,0 +1,158 @@
import { Router, Request, Response } from 'express';
import { logger } from '../utils/logger.js';
import fs from 'fs/promises';
import path from 'path';
import { exec } from 'child_process';
import { promisify } from 'util';
const execAsync = promisify(exec);
const router = Router();
// Path to frpc config file
const FRPC_CONFIG_PATH = process.env.FRPC_CONFIG_PATH || '/app/data/frpc.toml';
const FRPC_CONTAINER_NAME = process.env.FRPC_CONTAINER_NAME || 'frpc';
// Update frpc configuration
router.post('/update-config', async (req: Request, res: Response) => {
try {
const { config } = req.body;
if (!config || typeof config !== 'string') {
res.status(400).json({
success: false,
message: 'Invalid config format'
});
return;
}
// Ensure directory exists
const dir = path.dirname(FRPC_CONFIG_PATH);
await fs.mkdir(dir, { recursive: true });
// Write config to file
await fs.writeFile(FRPC_CONFIG_PATH, config);
logger.info('frpc configuration updated successfully');
res.json({
success: true,
message: 'Configuration updated successfully'
});
} catch (error) {
logger.error('Failed to update frpc configuration:', error);
res.status(500).json({
success: false,
message: 'Failed to update configuration'
});
}
});
// Restart frpc container
router.post('/restart', async (req: Request, res: Response) => {
try {
// Check if container exists
const { stdout: containers } = await execAsync(`docker ps -a --filter "name=${FRPC_CONTAINER_NAME}" --format "{{.Names}}"`);
if (!containers.includes(FRPC_CONTAINER_NAME)) {
logger.warn(`frpc container ${FRPC_CONTAINER_NAME} not found`);
res.status(404).json({
success: false,
message: 'frpc container not found'
});
return;
}
// Restart the container
await execAsync(`docker restart ${FRPC_CONTAINER_NAME}`);
logger.info(`frpc container ${FRPC_CONTAINER_NAME} restarted successfully`);
res.json({
success: true,
message: 'frpc restarted successfully'
});
} catch (error) {
logger.error('Failed to restart frpc container:', error);
res.status(500).json({
success: false,
message: 'Failed to restart frpc'
});
}
});
// Get frpc status
router.get('/status', async (req: Request, res: Response) => {
try {
const { stdout } = await execAsync(`docker ps --filter "name=${FRPC_CONTAINER_NAME}" --format "{{.Names}} {{.Status}}"`);
const isRunning = stdout.trim().includes(FRPC_CONTAINER_NAME);
res.json({
container: FRPC_CONTAINER_NAME,
running: isRunning,
status: isRunning ? 'running' : 'stopped',
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Failed to get frpc status:', error);
res.status(500).json({
running: false,
status: 'error',
message: 'Failed to get frpc status'
});
}
});
// Get frpc logs
router.get('/logs', async (req: Request, res: Response) => {
try {
const lines = parseInt(req.query.lines as string) || 50;
const { stdout } = await execAsync(`docker logs --tail ${lines} ${FRPC_CONTAINER_NAME}`);
res.json({
logs: stdout,
timestamp: new Date().toISOString()
});
} catch (error) {
logger.error('Failed to get frpc logs:', error);
res.status(500).json({
logs: '',
message: 'Failed to get frpc logs'
});
}
});
// Push and restart - convenience endpoint
router.post('/push-and-restart', async (req: Request, res: Response) => {
try {
const { config } = req.body;
if (!config || typeof config !== 'string') {
res.status(400).json({
success: false,
message: 'Invalid config format'
});
return;
}
// Update config
const dir = path.dirname(FRPC_CONFIG_PATH);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(FRPC_CONFIG_PATH, config);
// Restart frpc
await execAsync(`docker restart ${FRPC_CONTAINER_NAME}`);
logger.info('frpc configuration updated and container restarted');
res.json({
success: true,
message: 'Configuration updated and frpc restarted successfully'
});
} catch (error) {
logger.error('Failed to push config and restart frpc:', error);
res.status(500).json({
success: false,
message: 'Failed to update configuration and restart frpc'
});
}
});
export { router as frpcRouter };

View file

@ -0,0 +1,138 @@
import { Router, Request, Response } from 'express';
import { DockerManager } from '../services/dockerManager.js';
import { logger } from '../utils/logger.js';
const router = Router();
const dockerManager = new DockerManager();
// List all available game servers
router.get('/list', async (req: Request, res: Response) => {
try {
const servers = await dockerManager.listGameServers();
res.json(servers);
} catch (error) {
logger.error('Error listing game servers:', error);
res.status(500).json({ error: 'Failed to list game servers' });
}
});
// Start a game server
router.post('/start/:serviceName', async (req: Request, res: Response) => {
try {
const { serviceName } = req.params;
logger.info(`Request to start game server: ${serviceName}`);
const result = await dockerManager.startServer(serviceName);
if (result.success) {
res.json({
success: true,
message: result.message,
serviceName
});
} else {
res.status(400).json({
success: false,
message: result.message,
serviceName
});
}
} catch (error) {
logger.error(`Error starting game server ${req.params.serviceName}:`, error);
res.status(500).json({
success: false,
message: 'Internal server error',
serviceName: req.params.serviceName
});
}
});
// Stop a game server
router.post('/stop/:serviceName', async (req: Request, res: Response) => {
try {
const { serviceName } = req.params;
logger.info(`Request to stop game server: ${serviceName}`);
const result = await dockerManager.stopServer(serviceName);
if (result.success) {
res.json({
success: true,
message: result.message,
serviceName
});
} else {
res.status(400).json({
success: false,
message: result.message,
serviceName
});
}
} catch (error) {
logger.error(`Error stopping game server ${req.params.serviceName}:`, error);
res.status(500).json({
success: false,
message: 'Internal server error',
serviceName: req.params.serviceName
});
}
});
// Get status of a specific game server
router.get('/:serviceName/status', async (req: Request, res: Response) => {
try {
const { serviceName } = req.params;
const status = await dockerManager.getServerStatus(serviceName);
res.json(status);
} catch (error) {
logger.error(`Error getting status for ${req.params.serviceName}:`, error);
res.status(500).json({ error: 'Failed to get server status' });
}
});
// Restart a game server
router.post('/restart/:serviceName', async (req: Request, res: Response) => {
try {
const { serviceName } = req.params;
logger.info(`Request to restart game server: ${serviceName}`);
// Stop first
const stopResult = await dockerManager.stopServer(serviceName);
if (!stopResult.success && !stopResult.message.includes('not running')) {
return res.status(400).json({
success: false,
message: `Failed to stop ${serviceName}: ${stopResult.message}`,
serviceName
});
}
// Wait a moment
await new Promise(resolve => setTimeout(resolve, 2000));
// Start again
const startResult = await dockerManager.startServer(serviceName);
if (startResult.success) {
res.json({
success: true,
message: `${serviceName} restarted successfully`,
serviceName
});
} else {
res.status(400).json({
success: false,
message: `Failed to restart ${serviceName}: ${startResult.message}`,
serviceName
});
}
} catch (error) {
logger.error(`Error restarting game server ${req.params.serviceName}:`, error);
res.status(500).json({
success: false,
message: 'Internal server error',
serviceName: req.params.serviceName
});
}
});
export { router as gameServerRouter };

78
node/src/routes/status.ts Normal file
View file

@ -0,0 +1,78 @@
import { Router, Request, Response } from 'express';
import { DockerManager } from '../services/dockerManager.js';
import { logger } from '../utils/logger.js';
const router = Router();
const dockerManager = new DockerManager();
// Get overall server status
router.get('/', async (req: Request, res: Response) => {
try {
const [gameServers, systemStats, runningContainers] = await Promise.all([
dockerManager.listGameServers(),
dockerManager.getSystemStats(),
dockerManager.getRunningContainers()
]);
const activePorts = new Set<number>();
runningContainers.forEach((container: any) => {
if (container.Ports) {
container.Ports.forEach((port: any) => {
if (port.PublicPort) {
activePorts.add(port.PublicPort);
}
});
}
});
res.json({
timestamp: new Date().toISOString(),
status: 'operational',
gameServers: {
available: gameServers.available,
running: gameServers.running.length,
runningServers: gameServers.running
},
activePorts: Array.from(activePorts).sort(),
systemStats,
containers: {
total: runningContainers.length,
running: runningContainers.filter((c: any) => c.State === 'running').length
}
});
} catch (error) {
logger.error('Error getting server status:', error);
res.status(500).json({ error: 'Failed to get server status' });
}
});
// Get active ports and services
router.get('/ports', async (req: Request, res: Response) => {
try {
const containers = await dockerManager.getRunningContainers();
const portMappings: { [key: number]: string } = {};
containers.forEach((container: any) => {
if (container.Ports && container.State === 'running') {
container.Ports.forEach((port: any) => {
if (port.PublicPort) {
const serviceName = container.Labels?.['game-server'] ||
container.Names[0]?.replace('/', '') ||
'unknown';
portMappings[port.PublicPort] = serviceName;
}
});
}
});
res.json({
activePorts: portMappings,
totalPorts: Object.keys(portMappings).length
});
} catch (error) {
logger.error('Error getting port information:', error);
res.status(500).json({ error: 'Failed to get port information' });
}
});
export { router as statusRouter };

View file

@ -0,0 +1,247 @@
import Docker from 'dockerode';
import { logger } from '../utils/logger.js';
export interface GameServerConfig {
name: string;
image: string;
ports: { [key: string]: number };
environment?: { [key: string]: string };
volumes?: string[];
restart?: string;
}
export interface ServerStatus {
name: string;
status: 'running' | 'stopped' | 'not-found';
containerId?: string;
ports?: { [key: string]: number };
uptime?: string;
}
export class DockerManager {
private docker: Docker;
private gameServers: Map<string, GameServerConfig>;
constructor() {
this.docker = new Docker();
this.gameServers = new Map();
this.initializeGameServers();
}
private initializeGameServers() {
// Define available game servers
const servers: GameServerConfig[] = [
{
name: 'minecraft',
image: 'itzg/minecraft-server:latest',
ports: { '25565': 25565 },
environment: {
EULA: 'TRUE',
TYPE: 'VANILLA',
MEMORY: '2G'
},
volumes: ['/data/minecraft:/data'],
restart: 'unless-stopped'
},
{
name: 'valheim',
image: 'lloesche/valheim-server:latest',
ports: { '2456': 2456, '2457': 2457 },
environment: {
SERVER_NAME: 'My Valheim Server',
WORLD_NAME: 'MyWorld',
SERVER_PASS: 'secret123'
},
volumes: ['/data/valheim:/config'],
restart: 'unless-stopped'
},
{
name: 'terraria',
image: 'ryshe/terraria:latest',
ports: { '7777': 7777 },
environment: {
WORLD: 'MyWorld',
PASSWORD: 'secret123'
},
volumes: ['/data/terraria:/world'],
restart: 'unless-stopped'
}
];
servers.forEach(server => {
this.gameServers.set(server.name, server);
});
}
async getRunningContainers(): Promise<any[]> {
try {
const containers = await this.docker.listContainers();
return containers;
} catch (error) {
logger.error('Error listing containers:', error);
return [];
}
}
async getServerStatus(serviceName: string): Promise<ServerStatus> {
try {
const containers = await this.docker.listContainers({ all: true });
const container = containers.find(c =>
c.Names.some(name => name.includes(serviceName)) ||
c.Labels?.['game-server'] === serviceName
);
if (!container) {
return {
name: serviceName,
status: 'not-found'
};
}
const status = container.State === 'running' ? 'running' : 'stopped';
const ports: { [key: string]: number } = {};
if (container.Ports) {
container.Ports.forEach(port => {
if (port.PublicPort) {
ports[port.PrivatePort.toString()] = port.PublicPort;
}
});
}
return {
name: serviceName,
status,
containerId: container.Id,
ports,
uptime: status === 'running' ? this.calculateUptime(container.Created) : undefined
};
} catch (error) {
logger.error(`Error getting status for ${serviceName}:`, error);
return {
name: serviceName,
status: 'not-found'
};
}
}
async startServer(serviceName: string): Promise<{ success: boolean; message: string }> {
try {
const config = this.gameServers.get(serviceName);
if (!config) {
return { success: false, message: `Unknown game server: ${serviceName}` };
}
// Check if container already exists
const status = await this.getServerStatus(serviceName);
if (status.status === 'running') {
return { success: false, message: `${serviceName} is already running` };
}
if (status.containerId) {
// Container exists but is stopped, start it
const container = this.docker.getContainer(status.containerId);
await container.start();
logger.info(`Started existing container for ${serviceName}`);
} else {
// Create new container
const containerOptions = {
Image: config.image,
name: `gameserver-${serviceName}`,
Labels: {
'game-server': serviceName
},
Env: config.environment ? Object.entries(config.environment).map(([k, v]) => `${k}=${v}`) : [],
ExposedPorts: Object.keys(config.ports).reduce((acc, port) => {
acc[`${port}/tcp`] = {};
return acc;
}, {} as any),
HostConfig: {
PortBindings: Object.entries(config.ports).reduce((acc, [privatePort, publicPort]) => {
acc[`${privatePort}/tcp`] = [{ HostPort: publicPort.toString() }];
return acc;
}, {} as any),
Binds: config.volumes || [],
RestartPolicy: {
Name: config.restart || 'unless-stopped'
}
}
};
const container = await this.docker.createContainer(containerOptions);
await container.start();
logger.info(`Created and started new container for ${serviceName}`);
}
return { success: true, message: `${serviceName} started successfully` };
} catch (error) {
logger.error(`Error starting ${serviceName}:`, error);
return { success: false, message: `Failed to start ${serviceName}: ${error}` };
}
}
async stopServer(serviceName: string): Promise<{ success: boolean; message: string }> {
try {
const status = await this.getServerStatus(serviceName);
if (status.status !== 'running') {
return { success: false, message: `${serviceName} is not running` };
}
if (status.containerId) {
const container = this.docker.getContainer(status.containerId);
await container.stop();
logger.info(`Stopped container for ${serviceName}`);
return { success: true, message: `${serviceName} stopped successfully` };
}
return { success: false, message: `Could not find container for ${serviceName}` };
} catch (error) {
logger.error(`Error stopping ${serviceName}:`, error);
return { success: false, message: `Failed to stop ${serviceName}: ${error}` };
}
}
async listGameServers(): Promise<{ available: string[]; running: ServerStatus[] }> {
const available = Array.from(this.gameServers.keys());
const running: ServerStatus[] = [];
for (const serverName of available) {
const status = await this.getServerStatus(serverName);
if (status.status === 'running') {
running.push(status);
}
}
return { available, running };
}
async getSystemStats(): Promise<any> {
try {
const info = await this.docker.info();
const version = await this.docker.version();
return {
dockerVersion: version.Version,
containers: info.Containers,
containersRunning: info.ContainersRunning,
containersPaused: info.ContainersPaused,
containersStopped: info.ContainersStopped,
images: info.Images,
memoryLimit: info.MemoryLimit,
swapLimit: info.SwapLimit,
cpus: info.NCPU,
architecture: info.Architecture
};
} catch (error) {
logger.error('Error getting system stats:', error);
return null;
}
}
private calculateUptime(created: number): string {
const now = Date.now();
const uptime = now - (created * 1000);
const hours = Math.floor(uptime / (1000 * 60 * 60));
const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60));
return `${hours}h ${minutes}m`;
}
}

26
node/src/utils/logger.ts Normal file
View file

@ -0,0 +1,26 @@
import winston from 'winston';
const { combine, timestamp, errors, json, simple, colorize } = winston.format;
export const logger = winston.createLogger({
level: process.env.LOG_LEVEL || 'info',
format: combine(
timestamp(),
errors({ stack: true }),
json()
),
transports: [
new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
new winston.transports.File({ filename: 'logs/combined.log' })
]
});
// If not in production, log to console as well
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: combine(
colorize(),
simple()
)
}));
}

67
node/test-api.js Normal file
View file

@ -0,0 +1,67 @@
#!/usr/bin/env node
// Simple test script to demonstrate the Home Server Agent functionality
import { execSync } from 'child_process';
const API_BASE = 'http://localhost:3000';
const API_TOKEN = 'dev-token-123';
console.log('🎮 Home Server Agent Test Script\n');
// Helper function to make API requests
function makeRequest(endpoint, method = 'GET', needsAuth = true) {
const url = `${API_BASE}${endpoint}`;
let cmd;
if (process.platform === 'win32') {
// Windows PowerShell
if (needsAuth) {
cmd = `powershell "Invoke-RestMethod -Uri '${url}' -Method ${method} -Headers @{'Authorization'='Bearer ${API_TOKEN}'}"`;
} else {
cmd = `powershell "Invoke-RestMethod -Uri '${url}' -Method ${method}"`;
}
} else {
// Unix-like systems
if (needsAuth) {
cmd = `curl -s -X ${method} -H "Authorization: Bearer ${API_TOKEN}" ${url}`;
} else {
cmd = `curl -s -X ${method} ${url}`;
}
}
try {
const result = execSync(cmd, { encoding: 'utf8' });
return result;
} catch (error) {
console.error(`Error making request to ${endpoint}:`, error.message);
return null;
}
}
// Test sequence
console.log('1. Testing health check (no auth required)...');
const healthResult = makeRequest('/health', 'GET', false);
console.log(' ✅ Health check result:', healthResult ? 'OK' : 'FAILED');
console.log('\n2. Testing server status...');
const statusResult = makeRequest('/api/status');
console.log(' ✅ Server status retrieved');
console.log('\n3. Testing game server list...');
const listResult = makeRequest('/api/gameserver/list');
console.log(' ✅ Available game servers retrieved');
console.log('\n4. Testing API documentation...');
const rootResult = makeRequest('/', 'GET', false);
console.log(' ✅ API documentation retrieved');
console.log('\n5. Testing specific game server status...');
const minecraftStatus = makeRequest('/api/gameserver/minecraft/status');
console.log(' ✅ Minecraft server status retrieved');
console.log('\n🎉 All tests completed successfully!');
console.log('\nNext steps:');
console.log('- Start a game server: POST /api/gameserver/start/minecraft');
console.log('- Stop a game server: POST /api/gameserver/stop/minecraft');
console.log('- Check active ports: GET /api/status/ports');
console.log('\nNote: Game servers require Docker to be running.');

43
node/tsconfig.json Normal file
View file

@ -0,0 +1,43 @@
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "node",
"lib": ["ES2022"],
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true,
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"noImplicitThis": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"allowUnusedLabels": false,
"allowUnreachableCode": false,
"exactOptionalPropertyTypes": true,
"noImplicitOverride": true,
"noPropertyAccessFromIndexSignature": false,
"noUncheckedSideEffectImports": false,
"types": ["node", "bun-types"]
},
"include": [
"src/**/*"
],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}