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
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:
commit
4169337dd0
68 changed files with 8726 additions and 0 deletions
15
node/.dockerignore
Normal file
15
node/.dockerignore
Normal 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
26
node/.env.example
Normal 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
34
node/.gitignore
vendored
Normal 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
380
node/DEPLOYMENT.md
Normal 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
56
node/Dockerfile
Normal 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
285
node/README.md
Normal 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
430
node/bun.lock
Normal 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=="],
|
||||
}
|
||||
}
|
||||
49
node/docker-compose.dev.yml
Normal file
49
node/docker-compose.dev.yml
Normal 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
153
node/docker-compose.yml
Normal 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
110
node/health-check.ps1
Normal 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
98
node/health-check.sh
Normal 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
|
||||
23
node/home-server-agent.service
Normal file
23
node/home-server-agent.service
Normal 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
|
||||
54
node/install-windows-service.ps1
Normal file
54
node/install-windows-service.ps1
Normal 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
34
node/package.json
Normal 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
83
node/src/index.ts
Normal 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);
|
||||
});
|
||||
25
node/src/middleware/auth.ts
Normal file
25
node/src/middleware/auth.ts
Normal 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
158
node/src/routes/frpc.ts
Normal 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 };
|
||||
138
node/src/routes/gameServer.ts
Normal file
138
node/src/routes/gameServer.ts
Normal 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
78
node/src/routes/status.ts
Normal 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 };
|
||||
247
node/src/services/dockerManager.ts
Normal file
247
node/src/services/dockerManager.ts
Normal 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
26
node/src/utils/logger.ts
Normal 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
67
node/test-api.js
Normal 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
43
node/tsconfig.json
Normal 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"
|
||||
]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue