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

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

6
app/.env Normal file
View file

@ -0,0 +1,6 @@
# Environment variables for FRP configuration
FRPC_SERVER_ADDR=127.0.0.1
FRPC_SERVER_PORT=7000
FRPC_TOKEN=your-secret-token
NODE_ENV=development
PORT=3000

22
app/.env.example Normal file
View file

@ -0,0 +1,22 @@
# Environment variables for FRP configuration
# Copy this file to .env and update the values
# FRP Server Configuration
FRPC_SERVER_ADDR=your-vps-ip-address
FRPC_SERVER_PORT=7000
FRPC_TOKEN=your-secret-token
# Application Configuration
NODE_ENV=production
PORT=3000
# Database Configuration (SQLite is used by default)
# DATABASE_PATH=./data/tunnels.db
# Logging Configuration
LOG_LEVEL=info
# Node Integration Configuration
NODE_URL=http://your-home-server:3001
NODE_TOKEN=your-node-secret-token
NODE_TIMEOUT=5000

24
app/.gitignore vendored Normal file
View file

@ -0,0 +1,24 @@
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
lerna-debug.log*
node_modules
dist
dist-ssr
*.local
# Editor directories and files
.vscode/*
!.vscode/extensions.json
.idea
.DS_Store
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?

336
app/DEPLOYMENT.md Normal file
View file

@ -0,0 +1,336 @@
# Deployment Guide
This guide covers different deployment scenarios for the FRP Manager application.
## Table of Contents
1. [Docker Compose Deployment (Recommended)](#docker-compose-deployment)
2. [Manual Deployment](#manual-deployment)
3. [Development Setup](#development-setup)
4. [Production Configuration](#production-configuration)
5. [Troubleshooting](#troubleshooting)
## Docker Compose Deployment
### Prerequisites
- Docker and Docker Compose installed
- Access to your VPS/server running FRP server
### Quick Start
1. **Clone and Setup**
```bash
git clone <repository-url>
cd frp-manager
```
2. **Configure Environment**
```bash
cp .env.example .env
```
Edit `.env` with your FRP server details:
```env
FRPC_SERVER_ADDR=your-vps-ip-address
FRPC_SERVER_PORT=7000
FRPC_TOKEN=your-secret-token
NODE_ENV=production
PORT=3000
```
3. **Deploy**
```bash
docker-compose up -d
```
4. **Access Application**
- Web Interface: http://localhost:3000
- API: http://localhost:3000/api
- Health Check: http://localhost:3000/health
### Docker Compose Configuration
The `docker-compose.yml` includes:
- **app**: Main application container (Express API + React frontend)
- **frpc**: FRP client container for tunnel management
Key volumes:
- `./data:/app/data` - Database and configuration files
- `./logs:/app/logs` - Application logs
- `/var/run/docker.sock:/var/run/docker.sock` - Docker socket for container management
### Updating the Application
```bash
# Pull latest changes
git pull origin main
# Rebuild and restart
docker-compose down
docker-compose up -d --build
```
## Manual Deployment
### Prerequisites
- Node.js 18+ installed
- Docker (for FRPC container)
- SQLite3
### Installation Steps
1. **Install Dependencies**
```bash
npm install
```
2. **Build Application**
```bash
npm run build
```
3. **Configure Environment**
```bash
cp .env.example .env
# Edit .env with your configuration
```
4. **Create Directories**
```bash
mkdir -p data logs
```
5. **Start Application**
```bash
# Development
npm run dev
# Production
npm start
```
### Process Management
For production deployment, use a process manager like PM2:
```bash
# Install PM2
npm install -g pm2
# Start application with PM2
pm2 start npm --name "frp-manager" -- start
# Save PM2 configuration
pm2 save
# Setup auto-restart on reboot
pm2 startup
```
### Reverse Proxy Setup
Example Nginx configuration:
```nginx
server {
listen 80;
server_name your-domain.com;
location / {
proxy_pass http://localhost:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
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;
proxy_cache_bypass $http_upgrade;
}
}
```
## Development Setup
### Quick Start
1. **Install Dependencies**
```bash
npm install
```
2. **Create Environment File**
```bash
cp .env.example .env
```
3. **Start Development Server**
```bash
npm run dev
```
### Development Features
- **Hot Reload**: Both frontend and backend auto-reload on changes
- **Debug Logging**: Enhanced logging in development mode
- **React DevTools**: Included React Query DevTools
- **Error Handling**: Detailed error messages in development
### Project Structure
```
app/
├── src/
│ ├── client/ # React frontend
│ │ ├── api/ # API client and types
│ │ ├── components/ # Reusable React components
│ │ ├── pages/ # Page components
│ │ └── App.tsx # Main App component
│ └── server/ # Express backend
│ ├── database.ts # SQLite database operations
│ ├── frpc-manager.ts # FRPC container management
│ ├── logger.ts # Winston logging configuration
│ ├── main.ts # Express server setup
│ ├── routes.ts # API route definitions
│ └── types.ts # TypeScript type definitions
├── public/ # Static assets
├── data/ # Database and FRPC config
├── logs/ # Application logs
├── Dockerfile # Docker container definition
├── docker-compose.yml # Docker Compose configuration
└── package.json # Project dependencies and scripts
```
## Production Configuration
### Environment Variables
```env
# FRP Server Configuration
FRPC_SERVER_ADDR=your-vps-ip-address
FRPC_SERVER_PORT=7000
FRPC_TOKEN=your-secret-token
# Application Configuration
NODE_ENV=production
PORT=3000
# Security (optional)
CORS_ORIGIN=https://your-domain.com
```
### Security Considerations
1. **Authentication**: Consider adding authentication for production use
2. **CORS**: Configure appropriate CORS settings
3. **HTTPS**: Use HTTPS in production
4. **Firewall**: Restrict access to necessary ports only
5. **Docker Security**: Run containers with non-root users
### Monitoring
The application includes:
- **Health Check Endpoint**: `/health`
- **Structured Logging**: JSON logs with Winston
- **Error Tracking**: Comprehensive error logging
- **Service Status**: Built-in service monitoring
### Backup Strategy
Important data to backup:
- Database: `./data/tunnels.db`
- Configuration: `./data/frpc.toml`
- Environment: `.env`
- Logs: `./logs/` (optional)
Example backup script:
```bash
#!/bin/bash
BACKUP_DIR="./backups/$(date +%Y%m%d_%H%M%S)"
mkdir -p "$BACKUP_DIR"
cp -r ./data "$BACKUP_DIR/"
cp .env "$BACKUP_DIR/"
tar -czf "$BACKUP_DIR.tar.gz" "$BACKUP_DIR"
rm -rf "$BACKUP_DIR"
```
## Troubleshooting
### Common Issues
#### Application Won't Start
- Check Node.js version (requires 18+)
- Verify all dependencies are installed
- Check port availability (default: 3000)
- Review logs in `./logs/error.log`
#### Database Issues
- Ensure `./data` directory exists and is writable
- Check SQLite permissions
- Verify database file integrity
#### FRPC Container Issues
- Verify Docker is running
- Check FRPC container logs: `docker logs frpc`
- Ensure FRPC configuration is valid
- Verify server connectivity
#### API Errors
- Check server logs for detailed error messages
- Verify API endpoints are accessible
- Check CORS configuration for frontend issues
### Health Check Script
Use the provided health check script to verify all services:
```bash
# Linux/macOS
./health-check.sh
# Windows
powershell -ExecutionPolicy Bypass -File "health-check.ps1"
```
### Log Analysis
Application logs are stored in `./logs/`:
- `combined.log`: All application logs
- `error.log`: Error logs only
Example log analysis:
```bash
# View recent errors
tail -f ./logs/error.log
# Search for specific errors
grep "Failed to" ./logs/combined.log
# View API requests
grep "API Request" ./logs/combined.log
```
### Support
For additional support:
1. Check the [README.md](README.md) file
2. Review application logs
3. Use the health check script
4. Open an issue on the repository
### Performance Tuning
For high-traffic deployments:
1. Use a reverse proxy (Nginx, Apache)
2. Enable HTTP/2
3. Implement caching strategies
4. Monitor resource usage
5. Scale horizontally if needed
### Updates and Maintenance
Regular maintenance tasks:
- Update dependencies: `npm update`
- Backup database regularly
- Monitor disk space for logs
- Review and rotate logs
- Update Docker images: `docker-compose pull`

29
app/Dockerfile Normal file
View file

@ -0,0 +1,29 @@
# Use Node.js 18 LTS
FROM node:18-alpine
# Set working directory
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm ci --only=production
# Copy source code
COPY . .
# Build the application
RUN npm run build
# Create directories for data and logs
RUN mkdir -p data logs
# Expose port
EXPOSE 3000
# Install Docker CLI to manage frpc container
RUN apk add --no-cache docker-cli
# Start the application
CMD ["npm", "start"]

270
app/README.md Normal file
View file

@ -0,0 +1,270 @@
# FRP Manager
A fullstack application for managing FRP (Fast Reverse Proxy) tunnel configurations with a React frontend and Express backend. Now with **Node Integration** for remote management of home server FRP clients.
## Features
- 🚀 **Web Dashboard**: Modern React-based interface for managing tunnels
- 🔧 **Tunnel Management**: Create, edit, delete, and toggle tunnel configurations
- 📊 **Real-time Status**: Monitor tunnel status and FRPC service health
- 🏠 **Node Integration**: Remote control of home server FRP clients
- <20> **Push to Node**: Deploy configurations to remote nodes with one click
- 🌐 **Live Monitoring**: Real-time node connectivity and status tracking
- <20>🐳 **Docker Support**: Complete Docker Compose setup for easy deployment
- 📝 **Auto Configuration**: Automatically generates FRPC configuration from active tunnels
- 🗄️ **SQLite Database**: Persistent storage for tunnel configurations
- 📋 **Service Logs**: View FRPC service logs directly from the web interface
## Tech Stack
**Frontend:**
- React 19
- TypeScript
- React Router
- TanStack Query (React Query)
- Lucide React Icons
- React Hot Toast
**Backend:**
- Node.js
- Express
- TypeScript
- SQLite (better-sqlite3)
- Zod validation
- Winston logging
- Axios (for node communication)
**Infrastructure:**
- Docker & Docker Compose
- FRPC (Fast Reverse Proxy Client)
## New: Node Integration
The FRP Manager now supports remote management of home server nodes. This allows you to:
- **Query Status**: Get real-time status from your home server node
- **Push Configs**: Send updated frpc.toml configurations remotely
- **Restart Services**: Restart FRP client on the home server
- **Monitor Connectivity**: Live status indicators and connection tracking
### Setup Node Integration
1. **Configure the app** with node connection details in `.env`:
```bash
NODE_URL=http://your-home-server:3001
NODE_TOKEN=your-node-secret-token
NODE_TIMEOUT=5000
```
2. **Set up the home server node** (see `../node/` directory)
3. **Use the web interface** to push configurations with the "Push to Node" button
For detailed setup instructions, see [NODE_INTEGRATION.md](../NODE_INTEGRATION.md).
## Quick Start
### Using Docker Compose (Recommended)
1. Clone the repository:
```bash
git clone <repository-url>
cd frp-manager
```
2. Create environment configuration:
```bash
cp .env.example .env
```
3. Edit `.env` with your FRP server details:
```bash
FRPC_SERVER_ADDR=your-vps-ip-address
FRPC_SERVER_PORT=7000
FRPC_TOKEN=your-secret-token
```
4. Start the application:
```bash
docker-compose up -d
```
5. Access the web interface at `http://localhost:3000`
### Manual Installation
1. Install dependencies:
```bash
npm install
```
2. Set up environment variables:
```bash
cp .env.example .env
# Edit .env with your configuration
```
3. Start development server:
```bash
npm run dev
```
4. Access the application at `http://localhost:3000`
## Configuration
### Environment Variables
| Variable | Description | Default |
|----------|-------------|---------|
| `FRPC_SERVER_ADDR` | FRP server IP address | `your-vps-ip` |
| `FRPC_SERVER_PORT` | FRP server port | `7000` |
| `FRPC_TOKEN` | FRP authentication token | - |
| `NODE_ENV` | Node environment | `development` |
| `PORT` | Application port | `3000` |
### Tunnel Configuration
Each tunnel requires:
- **Name**: Descriptive name (e.g., "Minecraft Server")
- **Protocol**: TCP or UDP
- **Local IP**: IP address of the service on your network
- **Local Port**: Port of the service on your network
- **Remote Port**: Port to expose on the FRP server
- **Enabled**: Whether the tunnel is active
## Usage
### Adding a Tunnel
1. Navigate to the "Tunnels" page
2. Click "Add Tunnel"
3. Fill in the tunnel details:
- Name: "Minecraft Server"
- Protocol: TCP
- Local IP: 192.168.1.100
- Local Port: 25565
- Remote Port: 25565
- Enabled: ✓
4. Click "Create"
### Managing Tunnels
- **Edit**: Click the edit button to modify tunnel settings
- **Toggle**: Use the power button to enable/disable tunnels
- **Delete**: Remove tunnels you no longer need
- **Status**: View real-time tunnel status on the dashboard
### Service Management
Use the "Server Status" page to:
- Start/Stop/Restart the FRPC service
- Regenerate FRPC configuration
- View service logs
- Monitor system health
## API Endpoints
### Tunnels
- `GET /api/tunnels` - Get all tunnels
- `POST /api/tunnels` - Create new tunnel
- `GET /api/tunnels/:id` - Get tunnel by ID
- `PUT /api/tunnels/:id` - Update tunnel
- `DELETE /api/tunnels/:id` - Delete tunnel
- `GET /api/tunnels/:id/status` - Get tunnel status
### FRPC Service
- `GET /api/frpc/status` - Get service status
- `POST /api/frpc/start` - Start service
- `POST /api/frpc/stop` - Stop service
- `POST /api/frpc/restart` - Restart service
- `POST /api/frpc/regenerate` - Regenerate configuration
- `GET /api/frpc/logs` - Get service logs
## Development
### Project Structure
```
app/
├── src/
│ ├── client/ # React frontend
│ │ ├── api/ # API client
│ │ ├── components/ # React components
│ │ ├── pages/ # Page components
│ │ └── App.tsx # Main App component
│ └── server/ # Express backend
│ ├── database.ts # Database layer
│ ├── frpc-manager.ts # FRPC management
│ ├── logger.ts # Logging utilities
│ ├── main.ts # Server entry point
│ ├── routes.ts # API routes
│ └── types.ts # TypeScript types
├── public/ # Static assets
├── data/ # Database and config files
├── logs/ # Application logs
└── docker-compose.yml # Docker configuration
```
### Scripts
```bash
# Development
npm run dev # Start development server
# Production
npm run build # Build application
npm start # Start production server
# Docker
docker-compose up # Start all services
docker-compose down # Stop all services
```
### Contributing
1. Fork the repository
2. Create a feature branch
3. Make your changes
4. Test thoroughly
5. Submit a pull request
## Troubleshooting
### Common Issues
**FRPC service won't start:**
- Check if the server address and port are correct
- Verify the authentication token
- Ensure the FRPC container has the updated configuration
**Tunnels show as inactive:**
- Verify the local service is running
- Check firewall settings
- Ensure the local IP and port are correct
**Database errors:**
- Check if the `data` directory exists and is writable
- Verify SQLite permissions
### Logs
Application logs are stored in the `logs` directory:
- `combined.log`: All application logs
- `error.log`: Error logs only
FRPC logs can be viewed through the web interface or directly:
```bash
docker logs frpc
```
## License
MIT License - see LICENSE file for details.
## Support
For issues and questions:
1. Check the troubleshooting section
2. Review the logs for error messages
3. Open an issue on the repository

862
app/bun.lock Normal file
View file

@ -0,0 +1,862 @@
{
"lockfileVersion": 1,
"workspaces": {
"": {
"name": "arc-frp",
"dependencies": {
"@tanstack/react-query": "^5.13.4",
"@tanstack/react-query-devtools": "^5.13.5",
"axios": "^1.6.2",
"better-sqlite3": "^9.2.2",
"cors": "^2.8.5",
"cross-env": "^7.0.3",
"express": "^5.1.0",
"lucide-react": "^0.294.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"react-hot-toast": "^2.4.1",
"react-router-dom": "^6.20.1",
"sqlite3": "^5.1.6",
"tsx": "^4.19.3",
"typescript": "^5.8.3",
"uuid": "^9.0.1",
"vite-express": "*",
"winston": "^3.11.0",
"zod": "^3.22.4",
},
"devDependencies": {
"@types/better-sqlite3": "^7.6.8",
"@types/cors": "^2.8.17",
"@types/express": "^5.0.1",
"@types/node": "^22.15.2",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@types/uuid": "^9.0.7",
"@vitejs/plugin-react": "^4.4.1",
"nodemon": "^3.1.10",
"vite": "^6.3.3",
},
},
},
"packages": {
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="],
"@babel/compat-data": ["@babel/compat-data@7.28.0", "", {}, "sha512-60X7qkglvrap8mn1lh2ebxXdZYtUcpd7gsmy9kLaBJ4i/WdY8PqTSdxyA8qraikqKQK5C1KRBKXqznrVapyNaw=="],
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"@babel/generator": ["@babel/generator@7.28.0", "", { "dependencies": { "@babel/parser": "^7.28.0", "@babel/types": "^7.28.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-lJjzvrbEeWrhB4P3QBsH7tey117PjLZnDbLiQEKjQ/fNJTjuq4HSqgFA+UNSwZT8D7dxxbnuSBMsa1lrWzKlQg=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.27.2", "", { "dependencies": { "@babel/compat-data": "^7.27.2", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ=="],
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.27.3", "", { "dependencies": { "@babel/helper-module-imports": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1", "@babel/traverse": "^7.27.3" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.27.1", "", {}, "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.27.1", "", {}, "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="],
"@babel/helpers": ["@babel/helpers@7.27.6", "", { "dependencies": { "@babel/template": "^7.27.2", "@babel/types": "^7.27.6" } }, "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug=="],
"@babel/parser": ["@babel/parser@7.28.0", "", { "dependencies": { "@babel/types": "^7.28.0" }, "bin": "./bin/babel-parser.js" }, "sha512-jVZGvOxOuNSsuQuLRTh13nU0AogFlw32w/MT+LV6D3sP5WdbW61E77RnkbaO2dUvmPAYrBDJXGn5gGS6tH4j8g=="],
"@babel/plugin-transform-react-jsx-self": ["@babel/plugin-transform-react-jsx-self@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw=="],
"@babel/plugin-transform-react-jsx-source": ["@babel/plugin-transform-react-jsx-source@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw=="],
"@babel/template": ["@babel/template@7.27.2", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/parser": "^7.27.2", "@babel/types": "^7.27.1" } }, "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw=="],
"@babel/traverse": ["@babel/traverse@7.28.0", "", { "dependencies": { "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/types": "^7.28.0", "debug": "^4.3.1" } }, "sha512-mGe7UK5wWyh0bKRfupsUchrQGqvDbZDbKJw+kcRGSmdHVYrv+ltd0pnpDTVpiTqnaBru9iEvA8pz8W46v0Amwg=="],
"@babel/types": ["@babel/types@7.28.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.27.1" } }, "sha512-jYnje+JyZG5YThjHiF28oT4SIZLnYOcSBb6+SDaFIyzDVSkXQmQQYclJ2R+YxcdmK0AX6x1E5OQNtuh3jHDrUg=="],
"@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=="],
"@esbuild/aix-ppc64": ["@esbuild/aix-ppc64@0.25.5", "", { "os": "aix", "cpu": "ppc64" }, "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA=="],
"@esbuild/android-arm": ["@esbuild/android-arm@0.25.5", "", { "os": "android", "cpu": "arm" }, "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA=="],
"@esbuild/android-arm64": ["@esbuild/android-arm64@0.25.5", "", { "os": "android", "cpu": "arm64" }, "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg=="],
"@esbuild/android-x64": ["@esbuild/android-x64@0.25.5", "", { "os": "android", "cpu": "x64" }, "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw=="],
"@esbuild/darwin-arm64": ["@esbuild/darwin-arm64@0.25.5", "", { "os": "darwin", "cpu": "arm64" }, "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ=="],
"@esbuild/darwin-x64": ["@esbuild/darwin-x64@0.25.5", "", { "os": "darwin", "cpu": "x64" }, "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ=="],
"@esbuild/freebsd-arm64": ["@esbuild/freebsd-arm64@0.25.5", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw=="],
"@esbuild/freebsd-x64": ["@esbuild/freebsd-x64@0.25.5", "", { "os": "freebsd", "cpu": "x64" }, "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw=="],
"@esbuild/linux-arm": ["@esbuild/linux-arm@0.25.5", "", { "os": "linux", "cpu": "arm" }, "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw=="],
"@esbuild/linux-arm64": ["@esbuild/linux-arm64@0.25.5", "", { "os": "linux", "cpu": "arm64" }, "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg=="],
"@esbuild/linux-ia32": ["@esbuild/linux-ia32@0.25.5", "", { "os": "linux", "cpu": "ia32" }, "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA=="],
"@esbuild/linux-loong64": ["@esbuild/linux-loong64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg=="],
"@esbuild/linux-mips64el": ["@esbuild/linux-mips64el@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg=="],
"@esbuild/linux-ppc64": ["@esbuild/linux-ppc64@0.25.5", "", { "os": "linux", "cpu": "ppc64" }, "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ=="],
"@esbuild/linux-riscv64": ["@esbuild/linux-riscv64@0.25.5", "", { "os": "linux", "cpu": "none" }, "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA=="],
"@esbuild/linux-s390x": ["@esbuild/linux-s390x@0.25.5", "", { "os": "linux", "cpu": "s390x" }, "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ=="],
"@esbuild/linux-x64": ["@esbuild/linux-x64@0.25.5", "", { "os": "linux", "cpu": "x64" }, "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw=="],
"@esbuild/netbsd-arm64": ["@esbuild/netbsd-arm64@0.25.5", "", { "os": "none", "cpu": "arm64" }, "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw=="],
"@esbuild/netbsd-x64": ["@esbuild/netbsd-x64@0.25.5", "", { "os": "none", "cpu": "x64" }, "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ=="],
"@esbuild/openbsd-arm64": ["@esbuild/openbsd-arm64@0.25.5", "", { "os": "openbsd", "cpu": "arm64" }, "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw=="],
"@esbuild/openbsd-x64": ["@esbuild/openbsd-x64@0.25.5", "", { "os": "openbsd", "cpu": "x64" }, "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg=="],
"@esbuild/sunos-x64": ["@esbuild/sunos-x64@0.25.5", "", { "os": "sunos", "cpu": "x64" }, "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA=="],
"@esbuild/win32-arm64": ["@esbuild/win32-arm64@0.25.5", "", { "os": "win32", "cpu": "arm64" }, "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw=="],
"@esbuild/win32-ia32": ["@esbuild/win32-ia32@0.25.5", "", { "os": "win32", "cpu": "ia32" }, "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ=="],
"@esbuild/win32-x64": ["@esbuild/win32-x64@0.25.5", "", { "os": "win32", "cpu": "x64" }, "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g=="],
"@gar/promisify": ["@gar/promisify@1.1.3", "", {}, "sha512-k2Ty1JcVojjJFwrg/ThKi2ujJ7XNLYaFGNB/bWT9wGR+oSMJHMa5w+CUq6p/pVrKeNNgA7pCqEcjSnHVoqJQFw=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.12", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-OuLGC46TjB5BbN1dH8JULVVZY4WTdkF7tV9Ys6wLL1rubZnCMstOhNHueU5bLCrnRuDhKPDM4g6sw4Bel5Gzqg=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.4", "", {}, "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.29", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-uw6guiW/gcAGPDhLmd77/6lW8QLeiV5RUTsAX46Db6oLhGaVj4lhnPwb184s1bkc8kdVg/+h988dro8GRDpmYQ=="],
"@npmcli/fs": ["@npmcli/fs@1.1.1", "", { "dependencies": { "@gar/promisify": "^1.0.1", "semver": "^7.3.5" } }, "sha512-8KG5RD0GVP4ydEzRn/I4BNDuxDtqVbOdm8675T49OIG/NGhaK0pjPX7ZcDlvKYbA+ulvVK3ztfcF4uBdOxuJbQ=="],
"@npmcli/move-file": ["@npmcli/move-file@1.1.2", "", { "dependencies": { "mkdirp": "^1.0.4", "rimraf": "^3.0.2" } }, "sha512-1SUf/Cg2GzGDyaf15aR9St9TWlb+XvbZXWpDx8YKs7MLzMH/BCeopv+y9vzrzgkfykCGuWOlSu3mZhj2+FQcrg=="],
"@remix-run/router": ["@remix-run/router@1.23.0", "", {}, "sha512-O3rHJzAQKamUz1fvE0Qaw0xSFqsA/yafi2iqeE0pvdFtCO1viYx8QL6f3Ln/aCCTLxs68SLf0KPM9eSeM8yBnA=="],
"@rolldown/pluginutils": ["@rolldown/pluginutils@1.0.0-beta.19", "", {}, "sha512-3FL3mnMbPu0muGOCaKAhhFEYmqv9eTfPSJRJmANrCwtgK8VuxpsZDGK+m0LYAGoyO8+0j5uRe4PeyPDK1yA/hA=="],
"@rollup/rollup-android-arm-eabi": ["@rollup/rollup-android-arm-eabi@4.44.1", "", { "os": "android", "cpu": "arm" }, "sha512-JAcBr1+fgqx20m7Fwe1DxPUl/hPkee6jA6Pl7n1v2EFiktAHenTaXl5aIFjUIEsfn9w3HE4gK1lEgNGMzBDs1w=="],
"@rollup/rollup-android-arm64": ["@rollup/rollup-android-arm64@4.44.1", "", { "os": "android", "cpu": "arm64" }, "sha512-RurZetXqTu4p+G0ChbnkwBuAtwAbIwJkycw1n6GvlGlBuS4u5qlr5opix8cBAYFJgaY05TWtM+LaoFggUmbZEQ=="],
"@rollup/rollup-darwin-arm64": ["@rollup/rollup-darwin-arm64@4.44.1", "", { "os": "darwin", "cpu": "arm64" }, "sha512-fM/xPesi7g2M7chk37LOnmnSTHLG/v2ggWqKj3CCA1rMA4mm5KVBT1fNoswbo1JhPuNNZrVwpTvlCVggv8A2zg=="],
"@rollup/rollup-darwin-x64": ["@rollup/rollup-darwin-x64@4.44.1", "", { "os": "darwin", "cpu": "x64" }, "sha512-gDnWk57urJrkrHQ2WVx9TSVTH7lSlU7E3AFqiko+bgjlh78aJ88/3nycMax52VIVjIm3ObXnDL2H00e/xzoipw=="],
"@rollup/rollup-freebsd-arm64": ["@rollup/rollup-freebsd-arm64@4.44.1", "", { "os": "freebsd", "cpu": "arm64" }, "sha512-wnFQmJ/zPThM5zEGcnDcCJeYJgtSLjh1d//WuHzhf6zT3Md1BvvhJnWoy+HECKu2bMxaIcfWiu3bJgx6z4g2XA=="],
"@rollup/rollup-freebsd-x64": ["@rollup/rollup-freebsd-x64@4.44.1", "", { "os": "freebsd", "cpu": "x64" }, "sha512-uBmIxoJ4493YATvU2c0upGz87f99e3wop7TJgOA/bXMFd2SvKCI7xkxY/5k50bv7J6dw1SXT4MQBQSLn8Bb/Uw=="],
"@rollup/rollup-linux-arm-gnueabihf": ["@rollup/rollup-linux-arm-gnueabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-n0edDmSHlXFhrlmTK7XBuwKlG5MbS7yleS1cQ9nn4kIeW+dJH+ExqNgQ0RrFRew8Y+0V/x6C5IjsHrJmiHtkxQ=="],
"@rollup/rollup-linux-arm-musleabihf": ["@rollup/rollup-linux-arm-musleabihf@4.44.1", "", { "os": "linux", "cpu": "arm" }, "sha512-8WVUPy3FtAsKSpyk21kV52HCxB+me6YkbkFHATzC2Yd3yuqHwy2lbFL4alJOLXKljoRw08Zk8/xEj89cLQ/4Nw=="],
"@rollup/rollup-linux-arm64-gnu": ["@rollup/rollup-linux-arm64-gnu@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-yuktAOaeOgorWDeFJggjuCkMGeITfqvPgkIXhDqsfKX8J3jGyxdDZgBV/2kj/2DyPaLiX6bPdjJDTu9RB8lUPQ=="],
"@rollup/rollup-linux-arm64-musl": ["@rollup/rollup-linux-arm64-musl@4.44.1", "", { "os": "linux", "cpu": "arm64" }, "sha512-W+GBM4ifET1Plw8pdVaecwUgxmiH23CfAUj32u8knq0JPFyK4weRy6H7ooxYFD19YxBulL0Ktsflg5XS7+7u9g=="],
"@rollup/rollup-linux-loongarch64-gnu": ["@rollup/rollup-linux-loongarch64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-1zqnUEMWp9WrGVuVak6jWTl4fEtrVKfZY7CvcBmUUpxAJ7WcSowPSAWIKa/0o5mBL/Ij50SIf9tuirGx63Ovew=="],
"@rollup/rollup-linux-powerpc64le-gnu": ["@rollup/rollup-linux-powerpc64le-gnu@4.44.1", "", { "os": "linux", "cpu": "ppc64" }, "sha512-Rl3JKaRu0LHIx7ExBAAnf0JcOQetQffaw34T8vLlg9b1IhzcBgaIdnvEbbsZq9uZp3uAH+JkHd20Nwn0h9zPjA=="],
"@rollup/rollup-linux-riscv64-gnu": ["@rollup/rollup-linux-riscv64-gnu@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-j5akelU3snyL6K3N/iX7otLBIl347fGwmd95U5gS/7z6T4ftK288jKq3A5lcFKcx7wwzb5rgNvAg3ZbV4BqUSw=="],
"@rollup/rollup-linux-riscv64-musl": ["@rollup/rollup-linux-riscv64-musl@4.44.1", "", { "os": "linux", "cpu": "none" }, "sha512-ppn5llVGgrZw7yxbIm8TTvtj1EoPgYUAbfw0uDjIOzzoqlZlZrLJ/KuiE7uf5EpTpCTrNt1EdtzF0naMm0wGYg=="],
"@rollup/rollup-linux-s390x-gnu": ["@rollup/rollup-linux-s390x-gnu@4.44.1", "", { "os": "linux", "cpu": "s390x" }, "sha512-Hu6hEdix0oxtUma99jSP7xbvjkUM/ycke/AQQ4EC5g7jNRLLIwjcNwaUy95ZKBJJwg1ZowsclNnjYqzN4zwkAw=="],
"@rollup/rollup-linux-x64-gnu": ["@rollup/rollup-linux-x64-gnu@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-EtnsrmZGomz9WxK1bR5079zee3+7a+AdFlghyd6VbAjgRJDbTANJ9dcPIPAi76uG05micpEL+gPGmAKYTschQw=="],
"@rollup/rollup-linux-x64-musl": ["@rollup/rollup-linux-x64-musl@4.44.1", "", { "os": "linux", "cpu": "x64" }, "sha512-iAS4p+J1az6Usn0f8xhgL4PaU878KEtutP4hqw52I4IO6AGoyOkHCxcc4bqufv1tQLdDWFx8lR9YlwxKuv3/3g=="],
"@rollup/rollup-win32-arm64-msvc": ["@rollup/rollup-win32-arm64-msvc@4.44.1", "", { "os": "win32", "cpu": "arm64" }, "sha512-NtSJVKcXwcqozOl+FwI41OH3OApDyLk3kqTJgx8+gp6On9ZEt5mYhIsKNPGuaZr3p9T6NWPKGU/03Vw4CNU9qg=="],
"@rollup/rollup-win32-ia32-msvc": ["@rollup/rollup-win32-ia32-msvc@4.44.1", "", { "os": "win32", "cpu": "ia32" }, "sha512-JYA3qvCOLXSsnTR3oiyGws1Dm0YTuxAAeaYGVlGpUsHqloPcFjPg+X0Fj2qODGLNwQOAcCiQmHub/V007kiH5A=="],
"@rollup/rollup-win32-x64-msvc": ["@rollup/rollup-win32-x64-msvc@4.44.1", "", { "os": "win32", "cpu": "x64" }, "sha512-J8o22LuF0kTe7m+8PvW9wk3/bRq5+mRo5Dqo6+vXb7otCm3TPhYOJqOaQtGU9YMWQSL3krMnoOxMr0+9E6F3Ug=="],
"@tanstack/query-core": ["@tanstack/query-core@5.81.5", "", {}, "sha512-ZJOgCy/z2qpZXWaj/oxvodDx07XcQa9BF92c0oINjHkoqUPsmm3uG08HpTaviviZ/N9eP1f9CM7mKSEkIo7O1Q=="],
"@tanstack/query-devtools": ["@tanstack/query-devtools@5.81.2", "", {}, "sha512-jCeJcDCwKfoyyBXjXe9+Lo8aTkavygHHsUHAlxQKKaDeyT0qyQNLKl7+UyqYH2dDF6UN/14873IPBHchcsU+Zg=="],
"@tanstack/react-query": ["@tanstack/react-query@5.81.5", "", { "dependencies": { "@tanstack/query-core": "5.81.5" }, "peerDependencies": { "react": "^18 || ^19" } }, "sha512-lOf2KqRRiYWpQT86eeeftAGnjuTR35myTP8MXyvHa81VlomoAWNEd8x5vkcAfQefu0qtYCvyqLropFZqgI2EQw=="],
"@tanstack/react-query-devtools": ["@tanstack/react-query-devtools@5.81.5", "", { "dependencies": { "@tanstack/query-devtools": "5.81.2" }, "peerDependencies": { "@tanstack/react-query": "^5.81.5", "react": "^18 || ^19" } }, "sha512-lCGMu4RX0uGnlrlLeSckBfnW/UV+KMlTBVqa97cwK7Z2ED5JKnZRSjNXwoma6sQBTJrcULvzgx2K6jEPvNUpDw=="],
"@tootallnate/once": ["@tootallnate/once@1.1.2", "", {}, "sha512-RbzJvlNzmRq5c3O09UipeuXno4tA1FE6ikOjxZK0tuxVv3412l64l5t1W5pj4+rJq9vpkm/kwiR07aZXnsKPxw=="],
"@types/babel__core": ["@types/babel__core@7.20.5", "", { "dependencies": { "@babel/parser": "^7.20.7", "@babel/types": "^7.20.7", "@types/babel__generator": "*", "@types/babel__template": "*", "@types/babel__traverse": "*" } }, "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA=="],
"@types/babel__generator": ["@types/babel__generator@7.27.0", "", { "dependencies": { "@babel/types": "^7.0.0" } }, "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg=="],
"@types/babel__template": ["@types/babel__template@7.4.4", "", { "dependencies": { "@babel/parser": "^7.1.0", "@babel/types": "^7.0.0" } }, "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A=="],
"@types/babel__traverse": ["@types/babel__traverse@7.20.7", "", { "dependencies": { "@babel/types": "^7.20.7" } }, "sha512-dkO5fhS7+/oos4ciWxyEyjWe48zmG6wbCheo/G2ZnHx4fs3EU6YC6UM8rk56gAjNJ9P3MTH2jo5jb92/K6wbng=="],
"@types/better-sqlite3": ["@types/better-sqlite3@7.6.13", "", { "dependencies": { "@types/node": "*" } }, "sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA=="],
"@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="],
"@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/estree": ["@types/estree@1.0.8", "", {}, "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w=="],
"@types/express": ["@types/express@5.0.3", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^5.0.0", "@types/serve-static": "*" } }, "sha512-wGA0NX93b19/dZC1J18tKWVIYWyyF2ZjT9vin/NRu0qzzvfVzWjs04iq2rQ3H65vCTQYlRqs3YHfY7zjdV+9Kw=="],
"@types/express-serve-static-core": ["@types/express-serve-static-core@5.0.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-3xhRnjJPkULekpSzgtoNYYcTWgEZkp4myc+Saevii5JPnHNvHMRlBSHDbs7Bh1iPPoVTERHEZXyhyLbMEsExsA=="],
"@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/node": ["@types/node@22.16.0", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-B2egV9wALML1JCpv3VQoQ+yesQKAmNMBIAY7OteVrikcOcAkWm+dGL6qpeCktPjAv6N1JLnhbNiqS35UpFyBsQ=="],
"@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/react": ["@types/react@19.1.8", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-AwAfQ2Wa5bCx9WP8nZL2uMZWod7J7/JSplxbTmBQ5ms6QpqNYm672H0Vu9ZVKVngQ+ii4R/byguVEUZQyeg44g=="],
"@types/react-dom": ["@types/react-dom@19.1.6", "", { "peerDependencies": { "@types/react": "^19.0.0" } }, "sha512-4hOiT/dwO8Ko0gV1m/TJZYk3y0KBnY9vzDh7W+DH17b2HFSOGgdj33dhihPeuy3l0q23+4e+hoXHV6hCC4dCXw=="],
"@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=="],
"@types/uuid": ["@types/uuid@9.0.8", "", {}, "sha512-jg+97EGIcY9AGHJJRaaPVgetKDsrTgbRjQ5Msgjh/DQKEFl0DtyRr/VCOyD1T2R1MNeWPK/u7JoGhlDZnKBAfA=="],
"@vitejs/plugin-react": ["@vitejs/plugin-react@4.6.0", "", { "dependencies": { "@babel/core": "^7.27.4", "@babel/plugin-transform-react-jsx-self": "^7.27.1", "@babel/plugin-transform-react-jsx-source": "^7.27.1", "@rolldown/pluginutils": "1.0.0-beta.19", "@types/babel__core": "^7.20.5", "react-refresh": "^0.17.0" }, "peerDependencies": { "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0-beta.0" } }, "sha512-5Kgff+m8e2PB+9j51eGHEpn5kUzRKH2Ry0qGoe8ItJg7pqnkPrYPkDQZGgGmTa0EGarHrkjLvOdU3b1fzI8otQ=="],
"abbrev": ["abbrev@1.1.1", "", {}, "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q=="],
"accepts": ["accepts@2.0.0", "", { "dependencies": { "mime-types": "^3.0.0", "negotiator": "^1.0.0" } }, "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng=="],
"agent-base": ["agent-base@6.0.2", "", { "dependencies": { "debug": "4" } }, "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ=="],
"agentkeepalive": ["agentkeepalive@4.6.0", "", { "dependencies": { "humanize-ms": "^1.2.1" } }, "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ=="],
"aggregate-error": ["aggregate-error@3.1.0", "", { "dependencies": { "clean-stack": "^2.0.0", "indent-string": "^4.0.0" } }, "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA=="],
"ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"anymatch": ["anymatch@3.1.3", "", { "dependencies": { "normalize-path": "^3.0.0", "picomatch": "^2.0.4" } }, "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw=="],
"aproba": ["aproba@2.0.0", "", {}, "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ=="],
"are-we-there-yet": ["are-we-there-yet@3.0.1", "", { "dependencies": { "delegates": "^1.0.0", "readable-stream": "^3.6.0" } }, "sha512-QZW4EDmGwlYur0Yyf/b2uGucHQMa8aFUP7eu9ddR73vvhFyt4V0Vl3QHPcTNJ8l6qYOBdxgXdnBXQrHilfRQBg=="],
"async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="],
"asynckit": ["asynckit@0.4.0", "", {}, "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="],
"axios": ["axios@1.10.0", "", { "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", "proxy-from-env": "^1.1.0" } }, "sha512-/1xYAC4MP/HEG+3duIhFr4ZQXR4sQXOIe+o6sdqzeykGLx6Upp/1p8MHqhINOvGeP7xyNHe7tsiJByc4SSVUxw=="],
"balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"better-sqlite3": ["better-sqlite3@9.6.0", "", { "dependencies": { "bindings": "^1.5.0", "prebuild-install": "^7.1.1" } }, "sha512-yR5HATnqeYNVnkaUTf4bOP2dJSnyhP4puJN/QPRyx4YkBEEUxib422n2XzPqDEHjQQqazoYoADdAm5vE15+dAQ=="],
"binary-extensions": ["binary-extensions@2.3.0", "", {}, "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw=="],
"bindings": ["bindings@1.5.0", "", { "dependencies": { "file-uri-to-path": "1.0.0" } }, "sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ=="],
"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@2.2.0", "", { "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", "debug": "^4.4.0", "http-errors": "^2.0.0", "iconv-lite": "^0.6.3", "on-finished": "^2.4.1", "qs": "^6.14.0", "raw-body": "^3.0.0", "type-is": "^2.0.0" } }, "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg=="],
"brace-expansion": ["brace-expansion@1.1.12", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg=="],
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA=="],
"browserslist": ["browserslist@4.25.1", "", { "dependencies": { "caniuse-lite": "^1.0.30001726", "electron-to-chromium": "^1.5.173", "node-releases": "^2.0.19", "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" } }, "sha512-KGj0KoOMXLpSNkkEI6Z6mShmQy0bc1I+T7K9N81k4WWMrfz+6fQ6es80B/YLAeRoKvjYE1YSHHOW1qe9xIVzHw=="],
"buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="],
"bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="],
"cacache": ["cacache@15.3.0", "", { "dependencies": { "@npmcli/fs": "^1.0.0", "@npmcli/move-file": "^1.0.1", "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "glob": "^7.1.4", "infer-owner": "^1.0.4", "lru-cache": "^6.0.0", "minipass": "^3.1.1", "minipass-collect": "^1.0.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.2", "mkdirp": "^1.0.3", "p-map": "^4.0.0", "promise-inflight": "^1.0.1", "rimraf": "^3.0.2", "ssri": "^8.0.1", "tar": "^6.0.2", "unique-filename": "^1.1.1" } }, "sha512-VVdYzXEn+cnbXpFgWs5hTT7OScegHVmLhJIR8Ufqk3iFD6A6j5iSX1KuBTfNEv4tdJWE2PzA6IVFtcLC7fN9wQ=="],
"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=="],
"caniuse-lite": ["caniuse-lite@1.0.30001726", "", {}, "sha512-VQAUIUzBiZ/UnlM28fSp2CRF3ivUn1BWEvxMcVTNwpw91Py1pGbPIyIKtd+tzct9C3ouceCVdGAXxZOpZAsgdw=="],
"chokidar": ["chokidar@3.6.0", "", { "dependencies": { "anymatch": "~3.1.2", "braces": "~3.0.2", "glob-parent": "~5.1.2", "is-binary-path": "~2.1.0", "is-glob": "~4.0.1", "normalize-path": "~3.0.0", "readdirp": "~3.6.0" }, "optionalDependencies": { "fsevents": "~2.3.2" } }, "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw=="],
"chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="],
"clean-stack": ["clean-stack@2.2.0", "", {}, "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A=="],
"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=="],
"color-support": ["color-support@1.1.3", "", { "bin": { "color-support": "bin.js" } }, "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="],
"colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="],
"combined-stream": ["combined-stream@1.0.8", "", { "dependencies": { "delayed-stream": "~1.0.0" } }, "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg=="],
"concat-map": ["concat-map@0.0.1", "", {}, "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg=="],
"console-control-strings": ["console-control-strings@1.1.0", "", {}, "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ=="],
"content-disposition": ["content-disposition@1.0.0", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-Au9nRL8VNUut/XSzbQA38+M78dzP4D+eqg3gfJHMIHHYa3bg067xj1KxMUWj+VULbiZMowKngFFbKczUrNJ1mg=="],
"content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cookie": ["cookie@0.7.2", "", {}, "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w=="],
"cookie-signature": ["cookie-signature@1.2.2", "", {}, "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg=="],
"cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="],
"cross-env": ["cross-env@7.0.3", "", { "dependencies": { "cross-spawn": "^7.0.1" }, "bin": { "cross-env": "src/bin/cross-env.js", "cross-env-shell": "src/bin/cross-env-shell.js" } }, "sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="],
"debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="],
"decompress-response": ["decompress-response@6.0.0", "", { "dependencies": { "mimic-response": "^3.1.0" } }, "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ=="],
"deep-extend": ["deep-extend@0.6.0", "", {}, "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA=="],
"delayed-stream": ["delayed-stream@1.0.0", "", {}, "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ=="],
"delegates": ["delegates@1.0.0", "", {}, "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ=="],
"depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="],
"destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="],
"detect-libc": ["detect-libc@2.0.4", "", {}, "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA=="],
"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=="],
"electron-to-chromium": ["electron-to-chromium@1.5.179", "", {}, "sha512-UWKi/EbBopgfFsc5k61wFpV7WrnnSlSzW/e2XcBmS6qKYTivZlLtoll5/rdqRTxGglGHkmkW0j0pFNJG10EUIQ=="],
"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=="],
"encoding": ["encoding@0.1.13", "", { "dependencies": { "iconv-lite": "^0.6.2" } }, "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A=="],
"end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"err-code": ["err-code@2.0.3", "", {}, "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA=="],
"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=="],
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="],
"esbuild": ["esbuild@0.25.5", "", { "optionalDependencies": { "@esbuild/aix-ppc64": "0.25.5", "@esbuild/android-arm": "0.25.5", "@esbuild/android-arm64": "0.25.5", "@esbuild/android-x64": "0.25.5", "@esbuild/darwin-arm64": "0.25.5", "@esbuild/darwin-x64": "0.25.5", "@esbuild/freebsd-arm64": "0.25.5", "@esbuild/freebsd-x64": "0.25.5", "@esbuild/linux-arm": "0.25.5", "@esbuild/linux-arm64": "0.25.5", "@esbuild/linux-ia32": "0.25.5", "@esbuild/linux-loong64": "0.25.5", "@esbuild/linux-mips64el": "0.25.5", "@esbuild/linux-ppc64": "0.25.5", "@esbuild/linux-riscv64": "0.25.5", "@esbuild/linux-s390x": "0.25.5", "@esbuild/linux-x64": "0.25.5", "@esbuild/netbsd-arm64": "0.25.5", "@esbuild/netbsd-x64": "0.25.5", "@esbuild/openbsd-arm64": "0.25.5", "@esbuild/openbsd-x64": "0.25.5", "@esbuild/sunos-x64": "0.25.5", "@esbuild/win32-arm64": "0.25.5", "@esbuild/win32-ia32": "0.25.5", "@esbuild/win32-x64": "0.25.5" }, "bin": { "esbuild": "bin/esbuild" } }, "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ=="],
"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=="],
"expand-template": ["expand-template@2.0.3", "", {}, "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg=="],
"express": ["express@5.1.0", "", { "dependencies": { "accepts": "^2.0.0", "body-parser": "^2.2.0", "content-disposition": "^1.0.0", "content-type": "^1.0.5", "cookie": "^0.7.1", "cookie-signature": "^1.2.1", "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "finalhandler": "^2.1.0", "fresh": "^2.0.0", "http-errors": "^2.0.0", "merge-descriptors": "^2.0.0", "mime-types": "^3.0.0", "on-finished": "^2.4.1", "once": "^1.4.0", "parseurl": "^1.3.3", "proxy-addr": "^2.0.7", "qs": "^6.14.0", "range-parser": "^1.2.1", "router": "^2.2.0", "send": "^1.1.0", "serve-static": "^2.2.0", "statuses": "^2.0.1", "type-is": "^2.0.1", "vary": "^1.1.2" } }, "sha512-DT9ck5YIRU+8GYzzU5kT3eHGA5iL+1Zd0EutOmTE9Dtk+Tvuzd23VBU+ec7HPNSTxXYO55gPV/hq4pSBJDjFpA=="],
"express-static-gzip": ["express-static-gzip@2.2.0", "", { "dependencies": { "parseurl": "^1.3.3", "serve-static": "^1.16.2" } }, "sha512-4ZQ0pHX0CAauxmzry2/8XFLM6aZA4NBvg9QezSlsEO1zLnl7vMFa48/WIcjzdfOiEUS4S1npPPKP2NHHYAp6qg=="],
"fdir": ["fdir@6.4.6", "", { "peerDependencies": { "picomatch": "^3 || ^4" }, "optionalPeers": ["picomatch"] }, "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w=="],
"fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="],
"file-uri-to-path": ["file-uri-to-path@1.0.0", "", {}, "sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw=="],
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg=="],
"finalhandler": ["finalhandler@2.1.0", "", { "dependencies": { "debug": "^4.4.0", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "on-finished": "^2.4.1", "parseurl": "^1.3.3", "statuses": "^2.0.1" } }, "sha512-/t88Ty3d5JWQbWYgaOGCCYfXRwV1+be02WqYYlL6h0lEiUAMPM8o8qKGO01YIkOHzka2up08wvgYD0mDiI+q3Q=="],
"fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="],
"follow-redirects": ["follow-redirects@1.15.9", "", {}, "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ=="],
"form-data": ["form-data@4.0.3", "", { "dependencies": { "asynckit": "^0.4.0", "combined-stream": "^1.0.8", "es-set-tostringtag": "^2.1.0", "hasown": "^2.0.2", "mime-types": "^2.1.12" } }, "sha512-qsITQPfmvMOSAdeyZ+12I1c+CKSstAFAwu+97zrnWAbIr5u8wfsExUzCesVLC8NgHuRUqNN4Zy6UPWUTRGslcA=="],
"forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="],
"fresh": ["fresh@2.0.0", "", {}, "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A=="],
"fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="],
"fs-minipass": ["fs-minipass@2.1.0", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"fsevents": ["fsevents@2.3.3", "", { "os": "darwin" }, "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gauge": ["gauge@4.0.4", "", { "dependencies": { "aproba": "^1.0.3 || ^2.0.0", "color-support": "^1.1.3", "console-control-strings": "^1.1.0", "has-unicode": "^2.0.1", "signal-exit": "^3.0.7", "string-width": "^4.2.3", "strip-ansi": "^6.0.1", "wide-align": "^1.1.5" } }, "sha512-f9m+BEN5jkg6a0fZjleidjN51VE1X+mPFQ2DJ0uv1V39oCLCbsGe6yjbBnp7eK7z/+GAon99a3nHuqbuuthyPg=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"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=="],
"get-tsconfig": ["get-tsconfig@4.10.1", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ=="],
"github-from-package": ["github-from-package@0.0.0", "", {}, "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw=="],
"glob": ["glob@7.2.3", "", { "dependencies": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" } }, "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q=="],
"glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow=="],
"goober": ["goober@2.1.16", "", { "peerDependencies": { "csstype": "^3.0.10" } }, "sha512-erjk19y1U33+XAMe1VTvIONHYoSqE4iS7BYUZfHaqeohLmnC0FdxEh7rQU+6MZ4OajItzjZFSRtVANrQwNq6/g=="],
"gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="],
"graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="],
"has-flag": ["has-flag@3.0.0", "", {}, "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw=="],
"has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="],
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw=="],
"has-unicode": ["has-unicode@2.0.1", "", {}, "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ=="],
"hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"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=="],
"http-proxy-agent": ["http-proxy-agent@4.0.1", "", { "dependencies": { "@tootallnate/once": "1", "agent-base": "6", "debug": "4" } }, "sha512-k0zdNgqWTGA6aeIRVpvfVob4fL52dTfaehylg0Y4UvSySvOq/Y+BOyPrgpUrA7HylqvU8vIZGsRuXmspskV0Tg=="],
"https-proxy-agent": ["https-proxy-agent@5.0.1", "", { "dependencies": { "agent-base": "6", "debug": "4" } }, "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA=="],
"humanize-ms": ["humanize-ms@1.2.1", "", { "dependencies": { "ms": "^2.0.0" } }, "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ=="],
"iconv-lite": ["iconv-lite@0.6.3", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw=="],
"ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="],
"ignore-by-default": ["ignore-by-default@1.0.1", "", {}, "sha512-Ius2VYcGNk7T90CppJqcIkS5ooHUZyIQK+ClZfMfMNFEF9VSE73Fq+906u/CWu92x4gzZMWOwfFYckPObzdEbA=="],
"imurmurhash": ["imurmurhash@0.1.4", "", {}, "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA=="],
"indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="],
"infer-owner": ["infer-owner@1.0.4", "", {}, "sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A=="],
"inflight": ["inflight@1.0.6", "", { "dependencies": { "once": "^1.3.0", "wrappy": "1" } }, "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA=="],
"inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="],
"ini": ["ini@1.3.8", "", {}, "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew=="],
"ip-address": ["ip-address@9.0.5", "", { "dependencies": { "jsbn": "1.1.0", "sprintf-js": "^1.1.3" } }, "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g=="],
"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-binary-path": ["is-binary-path@2.1.0", "", { "dependencies": { "binary-extensions": "^2.0.0" } }, "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw=="],
"is-extglob": ["is-extglob@2.1.1", "", {}, "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg=="],
"is-lambda": ["is-lambda@1.0.1", "", {}, "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ=="],
"is-number": ["is-number@7.0.0", "", {}, "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng=="],
"is-promise": ["is-promise@4.0.0", "", {}, "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ=="],
"is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsbn": ["jsbn@1.1.0", "", {}, "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="],
"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=="],
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"lucide-react": ["lucide-react@0.294.0", "", { "peerDependencies": { "react": "^16.5.1 || ^17.0.0 || ^18.0.0" } }, "sha512-V7o0/VECSGbLHn3/1O67FUgBwWB+hmzshrgDVRJQhMh8uj5D3HBuIvhuAmQTtlupILSplwIZg5FTc4tTKMA2SA=="],
"make-fetch-happen": ["make-fetch-happen@9.1.0", "", { "dependencies": { "agentkeepalive": "^4.1.3", "cacache": "^15.2.0", "http-cache-semantics": "^4.1.0", "http-proxy-agent": "^4.0.1", "https-proxy-agent": "^5.0.0", "is-lambda": "^1.0.1", "lru-cache": "^6.0.0", "minipass": "^3.1.3", "minipass-collect": "^1.0.2", "minipass-fetch": "^1.3.2", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^0.6.2", "promise-retry": "^2.0.1", "socks-proxy-agent": "^6.0.0", "ssri": "^8.0.0" } }, "sha512-+zopwDy7DNknmwPQplem5lAZX/eCOzSvSNNcSKm5eVwTkOBzoktEfXsa9L23J/GIRhxRsaxzkPEhrJEpE2F4Gg=="],
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="],
"media-typer": ["media-typer@1.1.0", "", {}, "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw=="],
"merge-descriptors": ["merge-descriptors@2.0.0", "", {}, "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g=="],
"mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
"mime-types": ["mime-types@3.0.1", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA=="],
"mimic-response": ["mimic-response@3.1.0", "", {}, "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ=="],
"minimatch": ["minimatch@3.1.2", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw=="],
"minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="],
"minipass": ["minipass@5.0.0", "", {}, "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ=="],
"minipass-collect": ["minipass-collect@1.0.2", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-6T6lH0H8OG9kITm/Jm6tdooIbogG9e0tLgpY6mphXSm/A9u8Nq1ryBG+Qspiub9LjWlBPsPS3tWQ/Botq4FdxA=="],
"minipass-fetch": ["minipass-fetch@1.4.1", "", { "dependencies": { "minipass": "^3.1.0", "minipass-sized": "^1.0.3", "minizlib": "^2.0.0" }, "optionalDependencies": { "encoding": "^0.1.12" } }, "sha512-CGH1eblLq26Y15+Azk7ey4xh0J/XfJfrCox5LDJiKqI2Q2iwOLOKrlmIaODiSQS8d18jalF6y2K2ePUm0CmShw=="],
"minipass-flush": ["minipass-flush@1.0.5", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw=="],
"minipass-pipeline": ["minipass-pipeline@1.2.4", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A=="],
"minipass-sized": ["minipass-sized@1.0.3", "", { "dependencies": { "minipass": "^3.0.0" } }, "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g=="],
"minizlib": ["minizlib@2.1.2", "", { "dependencies": { "minipass": "^3.0.0", "yallist": "^4.0.0" } }, "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg=="],
"mkdirp": ["mkdirp@1.0.4", "", { "bin": { "mkdirp": "bin/cmd.js" } }, "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="],
"mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="],
"ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="],
"nanoid": ["nanoid@3.3.11", "", { "bin": { "nanoid": "bin/nanoid.cjs" } }, "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w=="],
"napi-build-utils": ["napi-build-utils@2.0.0", "", {}, "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA=="],
"negotiator": ["negotiator@1.0.0", "", {}, "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg=="],
"node-abi": ["node-abi@3.75.0", "", { "dependencies": { "semver": "^7.3.5" } }, "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg=="],
"node-addon-api": ["node-addon-api@7.1.1", "", {}, "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ=="],
"node-gyp": ["node-gyp@8.4.1", "", { "dependencies": { "env-paths": "^2.2.0", "glob": "^7.1.4", "graceful-fs": "^4.2.6", "make-fetch-happen": "^9.1.0", "nopt": "^5.0.0", "npmlog": "^6.0.0", "rimraf": "^3.0.2", "semver": "^7.3.5", "tar": "^6.1.2", "which": "^2.0.2" }, "bin": { "node-gyp": "bin/node-gyp.js" } }, "sha512-olTJRgUtAb/hOXG0E93wZDs5YiJlgbXxTwQAFHyNlRsXQnYzUaF2aGgujZbw+hR8aF4ZG/rST57bWMWD16jr9w=="],
"node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="],
"nodemon": ["nodemon@3.1.10", "", { "dependencies": { "chokidar": "^3.5.2", "debug": "^4", "ignore-by-default": "^1.0.1", "minimatch": "^3.1.2", "pstree.remy": "^1.1.8", "semver": "^7.5.3", "simple-update-notifier": "^2.0.0", "supports-color": "^5.5.0", "touch": "^3.1.0", "undefsafe": "^2.0.5" }, "bin": { "nodemon": "bin/nodemon.js" } }, "sha512-WDjw3pJ0/0jMFmyNDp3gvY2YizjLmmOUQo6DEBY+JgdvW/yQ9mEeSw6H5ythl5Ny2ytb7f9C2nIbjSxMNzbJXw=="],
"nopt": ["nopt@5.0.0", "", { "dependencies": { "abbrev": "1" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ=="],
"normalize-path": ["normalize-path@3.0.0", "", {}, "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA=="],
"npmlog": ["npmlog@6.0.2", "", { "dependencies": { "are-we-there-yet": "^3.0.0", "console-control-strings": "^1.1.0", "gauge": "^4.0.3", "set-blocking": "^2.0.0" } }, "sha512-/vBvz5Jfr9dT/aFWd0FIRf+T/Q2WBsLENygUaFUqstqsycmZAP/t5BvFJTK0viFmSUxiUKTUplWy5vt+rvKIxg=="],
"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=="],
"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=="],
"p-map": ["p-map@4.0.0", "", { "dependencies": { "aggregate-error": "^3.0.0" } }, "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ=="],
"parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="],
"path-is-absolute": ["path-is-absolute@1.0.1", "", {}, "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-to-regexp": ["path-to-regexp@8.2.0", "", {}, "sha512-TdrF7fW9Rphjq4RjrW0Kp2AW0Ahwu9sRGTkS6bvDi0SCwZlEZYmcfDbEsTz8RVk0EHIS/Vd1bv3JhG+1xZuAyQ=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.2", "", {}, "sha512-M7BAV6Rlcy5u+m6oPhAPFgJTzAioX/6B0DxyvDlo9l8+T3nLKbrczg2WLUyzd45L8RqfUMyGPzekbMvX2Ldkwg=="],
"postcss": ["postcss@8.5.6", "", { "dependencies": { "nanoid": "^3.3.11", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg=="],
"prebuild-install": ["prebuild-install@7.1.3", "", { "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", "github-from-package": "0.0.0", "minimist": "^1.2.3", "mkdirp-classic": "^0.5.3", "napi-build-utils": "^2.0.0", "node-abi": "^3.3.0", "pump": "^3.0.0", "rc": "^1.2.7", "simple-get": "^4.0.0", "tar-fs": "^2.0.0", "tunnel-agent": "^0.6.0" }, "bin": { "prebuild-install": "bin.js" } }, "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug=="],
"promise-inflight": ["promise-inflight@1.0.1", "", {}, "sha512-6zWPyEOFaQBJYcGMHBKTKJ3u6TBsnMFOIZSa6ce1e/ZrrsOlnHRHbabMjLiBYKp+n44X9eUI6VUPaukCXHuG4g=="],
"promise-retry": ["promise-retry@2.0.1", "", { "dependencies": { "err-code": "^2.0.2", "retry": "^0.12.0" } }, "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g=="],
"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=="],
"proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="],
"pstree.remy": ["pstree.remy@1.1.8", "", {}, "sha512-77DZwxQmxKnu3aR542U+X8FypNzbfJ+C5XQDk3uWjWxn6151aIMGthWYRXTqT1E5oJvg+ljaa2OJi+VfvCOQ8w=="],
"pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="],
"qs": ["qs@6.14.0", "", { "dependencies": { "side-channel": "^1.1.0" } }, "sha512-YWWTjgABSKcvs/nWBi9PycY/JiPJqOD4JA6o9Sej2AtvSGarXxKC3OQSk4pAarbdQlKAh5D4FCQkJNkW+GAn3w=="],
"range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="],
"raw-body": ["raw-body@3.0.0", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.6.3", "unpipe": "1.0.0" } }, "sha512-RmkhL8CAyCRPXCE28MMH0z2PNWQBNk2Q09ZdxM9IOOXwxwZbN+qbWaatPkdkWIKL2ZVDImrN/pK5HTRz2PcS4g=="],
"rc": ["rc@1.2.8", "", { "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", "minimist": "^1.2.0", "strip-json-comments": "~2.0.1" }, "bin": { "rc": "./cli.js" } }, "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw=="],
"react": ["react@19.1.0", "", {}, "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg=="],
"react-dom": ["react-dom@19.1.0", "", { "dependencies": { "scheduler": "^0.26.0" }, "peerDependencies": { "react": "^19.1.0" } }, "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g=="],
"react-hot-toast": ["react-hot-toast@2.5.2", "", { "dependencies": { "csstype": "^3.1.3", "goober": "^2.1.16" }, "peerDependencies": { "react": ">=16", "react-dom": ">=16" } }, "sha512-Tun3BbCxzmXXM7C+NI4qiv6lT0uwGh4oAfeJyNOjYUejTsm35mK9iCaYLGv8cBz9L5YxZLx/2ii7zsIwPtPUdw=="],
"react-refresh": ["react-refresh@0.17.0", "", {}, "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ=="],
"react-router": ["react-router@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0" }, "peerDependencies": { "react": ">=16.8" } }, "sha512-X1m21aEmxGXqENEPG3T6u0Th7g0aS4ZmoNynhbs+Cn+q+QGTLt+d5IQ2bHAXKzKcxGJjxACpVbnYQSCRcfxHlQ=="],
"react-router-dom": ["react-router-dom@6.30.1", "", { "dependencies": { "@remix-run/router": "1.23.0", "react-router": "6.30.1" }, "peerDependencies": { "react": ">=16.8", "react-dom": ">=16.8" } }, "sha512-llKsgOkZdbPU1Eg3zK8lCn+sjD9wMRZZPuzmdWWX5SUs8OFkN5HnFVC0u5KMeMaC9aoancFI/KoLuKPqN+hxHw=="],
"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=="],
"readdirp": ["readdirp@3.6.0", "", { "dependencies": { "picomatch": "^2.2.1" } }, "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA=="],
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw=="],
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
"rimraf": ["rimraf@3.0.2", "", { "dependencies": { "glob": "^7.1.3" }, "bin": { "rimraf": "bin.js" } }, "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA=="],
"rollup": ["rollup@4.44.1", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.44.1", "@rollup/rollup-android-arm64": "4.44.1", "@rollup/rollup-darwin-arm64": "4.44.1", "@rollup/rollup-darwin-x64": "4.44.1", "@rollup/rollup-freebsd-arm64": "4.44.1", "@rollup/rollup-freebsd-x64": "4.44.1", "@rollup/rollup-linux-arm-gnueabihf": "4.44.1", "@rollup/rollup-linux-arm-musleabihf": "4.44.1", "@rollup/rollup-linux-arm64-gnu": "4.44.1", "@rollup/rollup-linux-arm64-musl": "4.44.1", "@rollup/rollup-linux-loongarch64-gnu": "4.44.1", "@rollup/rollup-linux-powerpc64le-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-gnu": "4.44.1", "@rollup/rollup-linux-riscv64-musl": "4.44.1", "@rollup/rollup-linux-s390x-gnu": "4.44.1", "@rollup/rollup-linux-x64-gnu": "4.44.1", "@rollup/rollup-linux-x64-musl": "4.44.1", "@rollup/rollup-win32-arm64-msvc": "4.44.1", "@rollup/rollup-win32-ia32-msvc": "4.44.1", "@rollup/rollup-win32-x64-msvc": "4.44.1", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-x8H8aPvD+xbl0Do8oez5f5o8eMS3trfCghc4HhLAnCkj7Vl0d1JWGs0UF/D886zLW2rOj2QymV/JcSSsw+XDNg=="],
"router": ["router@2.2.0", "", { "dependencies": { "debug": "^4.4.0", "depd": "^2.0.0", "is-promise": "^4.0.0", "parseurl": "^1.3.3", "path-to-regexp": "^8.0.0" } }, "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ=="],
"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=="],
"scheduler": ["scheduler@0.26.0", "", {}, "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA=="],
"semver": ["semver@7.7.2", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA=="],
"send": ["send@1.2.0", "", { "dependencies": { "debug": "^4.3.5", "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "etag": "^1.8.1", "fresh": "^2.0.0", "http-errors": "^2.0.0", "mime-types": "^3.0.1", "ms": "^2.1.3", "on-finished": "^2.4.1", "range-parser": "^1.2.1", "statuses": "^2.0.1" } }, "sha512-uaW0WwXKpL9blXE2o0bRhoL2EGXIrZxQ2ZQ4mgcfoBxdFmQold+qWsD2jLrfZ0trjKL6vOw0j//eAwcALFjKSw=="],
"serve-static": ["serve-static@2.2.0", "", { "dependencies": { "encodeurl": "^2.0.0", "escape-html": "^1.0.3", "parseurl": "^1.3.3", "send": "^1.2.0" } }, "sha512-61g9pCh0Vnh7IutZjtLGGpTA355+OPn2TyDv/6ivP2h/AdAVX9azsoxmg2/M6nZeQZNYBEwIcsne1mJd9oQItQ=="],
"set-blocking": ["set-blocking@2.0.0", "", {}, "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw=="],
"setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
"shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="],
"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=="],
"signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
"simple-concat": ["simple-concat@1.0.1", "", {}, "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q=="],
"simple-get": ["simple-get@4.0.1", "", { "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", "simple-concat": "^1.0.0" } }, "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA=="],
"simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="],
"simple-update-notifier": ["simple-update-notifier@2.0.0", "", { "dependencies": { "semver": "^7.5.3" } }, "sha512-a2B9Y0KlNXl9u/vsW6sTIu9vGEpfKu2wRV6l1H3XEas/0gUIzGzBoP/IouTcUQbm9JWZLH3COxyn03TYlFax6w=="],
"smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="],
"socks": ["socks@2.8.5", "", { "dependencies": { "ip-address": "^9.0.5", "smart-buffer": "^4.2.0" } }, "sha512-iF+tNDQla22geJdTyJB1wM/qrX9DMRwWrciEPwWLPRWAUEM8sQiyxgckLxWT1f7+9VabJS0jTGGr4QgBuvi6Ww=="],
"socks-proxy-agent": ["socks-proxy-agent@6.2.1", "", { "dependencies": { "agent-base": "^6.0.2", "debug": "^4.3.3", "socks": "^2.6.2" } }, "sha512-a6KW9G+6B3nWZ1yB8G7pJwL3ggLy1uTzKAgCb7ttblwqdz9fMGJUuTy3uFzEP48FAs9FLILlmzDlE2JJhVQaXQ=="],
"source-map-js": ["source-map-js@1.2.1", "", {}, "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA=="],
"sprintf-js": ["sprintf-js@1.1.3", "", {}, "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA=="],
"sqlite3": ["sqlite3@5.1.7", "", { "dependencies": { "bindings": "^1.5.0", "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.1", "tar": "^6.1.11" }, "optionalDependencies": { "node-gyp": "8.x" } }, "sha512-GGIyOiFaG+TUra3JIfkI/zGP8yZYLPQ0pl1bH+ODjiX57sPhrLU5sQJn1y9bDKZUFYkX1crlrPfSYt0BKKdkog=="],
"ssri": ["ssri@8.0.1", "", { "dependencies": { "minipass": "^3.1.1" } }, "sha512-97qShzy1AiyxvPNIkLWoGua7xoQzzPjQ0HAH4B0rWKo7SZ6USuPcrUiAFrws0UH8RrbWmgq3LMTObhPIHbbBeQ=="],
"stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="],
"statuses": ["statuses@2.0.2", "", {}, "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw=="],
"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=="],
"strip-json-comments": ["strip-json-comments@2.0.1", "", {}, "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ=="],
"supports-color": ["supports-color@5.5.0", "", { "dependencies": { "has-flag": "^3.0.0" } }, "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow=="],
"tar": ["tar@6.2.1", "", { "dependencies": { "chownr": "^2.0.0", "fs-minipass": "^2.0.0", "minipass": "^5.0.0", "minizlib": "^2.1.1", "mkdirp": "^1.0.3", "yallist": "^4.0.0" } }, "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A=="],
"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=="],
"tinyglobby": ["tinyglobby@0.2.14", "", { "dependencies": { "fdir": "^6.4.4", "picomatch": "^4.0.2" } }, "sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ=="],
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="],
"toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="],
"touch": ["touch@3.1.1", "", { "bin": { "nodetouch": "bin/nodetouch.js" } }, "sha512-r0eojU4bI8MnHr8c5bNo7lJDdI2qXlWWJk6a9EAFG7vbhTjElYhBVS3/miuE0uOuoLdb8Mc/rVfsmm6eo5o9GA=="],
"triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="],
"tsx": ["tsx@4.20.3", "", { "dependencies": { "esbuild": "~0.25.0", "get-tsconfig": "^4.7.5" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "bin": { "tsx": "dist/cli.mjs" } }, "sha512-qjbnuR9Tr+FJOMBqJCW5ehvIo/buZq7vH7qD7JziU98h6l3qGy0a/yPFjwO+y0/T7GFpNgNAvEcPPVfyT8rrPQ=="],
"tunnel-agent": ["tunnel-agent@0.6.0", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w=="],
"type-is": ["type-is@2.0.1", "", { "dependencies": { "content-type": "^1.0.5", "media-typer": "^1.1.0", "mime-types": "^3.0.0" } }, "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw=="],
"typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="],
"undefsafe": ["undefsafe@2.0.5", "", {}, "sha512-WxONCrssBM8TSPRqN5EmsjVrsv4A8X12J4ArBiiayv3DyyG3ZlIg6yysuuSYdZsVz3TKcTg2fd//Ujd4CHV1iA=="],
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unique-filename": ["unique-filename@1.1.1", "", { "dependencies": { "unique-slug": "^2.0.0" } }, "sha512-Vmp0jIp2ln35UTXuryvjzkjGdRyf9b2lTXuSYUiPmzRcl3FDtYqAwOnTJkAngD9SWhnoJzDbTKwaOrZ+STtxNQ=="],
"unique-slug": ["unique-slug@2.0.2", "", { "dependencies": { "imurmurhash": "^0.1.4" } }, "sha512-zoWr9ObaxALD3DOPfjPSqxt4fnZiWblxHIgeWqW8x7UqDzEtHEQLzji2cuJYQFCU6KmoJikOYAZlrTHHebjx2w=="],
"unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="],
"update-browserslist-db": ["update-browserslist-db@1.1.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"uuid": ["uuid@9.0.1", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA=="],
"vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="],
"vite": ["vite@6.3.5", "", { "dependencies": { "esbuild": "^0.25.0", "fdir": "^6.4.4", "picomatch": "^4.0.2", "postcss": "^8.5.3", "rollup": "^4.34.9", "tinyglobby": "^0.2.13" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ=="],
"vite-express": ["vite-express@0.21.1", "", { "dependencies": { "express-static-gzip": "^2.2.0", "picocolors": "^1.1.1" } }, "sha512-/dz1syfdKfWwcNRSl9wxZQmH7dImrvxNR9TptbpYGqrlawWFD+USzbLR1ytWei8XJpDPDRUgOoT8dEIf/vviyQ=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wide-align": ["wide-align@1.1.5", "", { "dependencies": { "string-width": "^1.0.2 || 2 || 3 || 4" } }, "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg=="],
"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=="],
"wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="],
"yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"zod": ["zod@3.25.71", "", {}, "sha512-BsBc/NPk7h8WsUWYWYL+BajcJPY8YhjelaWu2NMLuzgraKAz4Lb4/6K11g9jpuDetjMiqhZ6YaexFLOC0Ogi3Q=="],
"@babel/core/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"@babel/helper-compilation-targets/semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"anymatch/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"cacache/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"cacache/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="],
"express-static-gzip/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=="],
"form-data/mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="],
"fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"http-errors/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"make-fetch-happen/lru-cache": ["lru-cache@6.0.0", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA=="],
"make-fetch-happen/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"make-fetch-happen/negotiator": ["negotiator@0.6.4", "", {}, "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w=="],
"minipass-collect/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-fetch/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-flush/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minipass-sized/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"minizlib/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"readdirp/picomatch": ["picomatch@2.3.1", "", {}, "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA=="],
"ssri/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"tar-fs/chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="],
"express-static-gzip/serve-static/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=="],
"form-data/mime-types/mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="],
"express-static-gzip/serve-static/send/debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="],
"express-static-gzip/serve-static/send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="],
"express-static-gzip/serve-static/send/fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="],
"express-static-gzip/serve-static/send/statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="],
"express-static-gzip/serve-static/send/debug/ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="],
}
}

15
app/data/frpc.toml Normal file
View file

@ -0,0 +1,15 @@
[common]
server_addr = "127.0.0.1"
server_port = 7000
token = "your-secret-token"
# Example tunnel configuration
# This file will be automatically generated by the FRP Manager
# based on your tunnel configurations in the web interface
# Example:
# [minecraft-server]
# type = "tcp"
# local_ip = "192.168.1.100"
# local_port = 25565
# remote_port = 25565

BIN
app/data/tunnels.db Normal file

Binary file not shown.

View file

@ -0,0 +1,28 @@
version: '3.8'
services:
app:
build: .
ports:
- "3000:3000"
volumes:
- ./data:/app/data
- ./logs:/app/logs
- /var/run/docker.sock:/var/run/docker.sock
environment:
- NODE_ENV=development
- FRPC_SERVER_ADDR=127.0.0.1
- FRPC_SERVER_PORT=7000
- FRPC_TOKEN=your-secret-token
depends_on:
- frpc
restart: unless-stopped
frpc:
image: snowdreamtech/frpc:latest
container_name: frpc
volumes:
- ./data/frpc.toml:/etc/frp/frpc.toml:ro
restart: unless-stopped
# Use host network for easier local development
network_mode: "host"

39
app/docker-compose.yml Normal file
View file

@ -0,0 +1,39 @@
version: '3.8'
services:
# Main application (Express API + React frontend)
app:
build: .
container_name: frp-manager
ports:
- "3000:3000"
volumes:
- ./data:/app/data
- ./logs:/app/logs
- /var/run/docker.sock:/var/run/docker.sock
environment:
- NODE_ENV=production
- FRPC_SERVER_ADDR=${FRPC_SERVER_ADDR:-your-vps-ip}
- FRPC_SERVER_PORT=${FRPC_SERVER_PORT:-7000}
- FRPC_TOKEN=${FRPC_TOKEN}
- NODE_URL=${NODE_URL}
- NODE_TOKEN=${NODE_TOKEN}
- NODE_TIMEOUT=${NODE_TIMEOUT:-5000}
depends_on:
- frpc
restart: unless-stopped
# FRPC client container
frpc:
image: snowdreamtech/frpc:latest
container_name: frpc
volumes:
- ./data/frpc.toml:/etc/frp/frpc.toml
depends_on:
- app
restart: unless-stopped
network_mode: "host"
volumes:
data:
logs:

69
app/health-check.ps1 Normal file
View file

@ -0,0 +1,69 @@
# Health check script for FRP Manager (PowerShell)
# This script checks if all services are running correctly
Write-Host "🔍 FRP Manager Health Check" -ForegroundColor Cyan
Write-Host "==========================" -ForegroundColor Cyan
# Check if main application is running
Write-Host "📡 Checking main application..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "http://localhost:3000/health" -UseBasicParsing -TimeoutSec 5
if ($response.StatusCode -eq 200) {
Write-Host "✅ Main application is running" -ForegroundColor Green
} else {
Write-Host "❌ Main application returned status code: $($response.StatusCode)" -ForegroundColor Red
exit 1
}
} catch {
Write-Host "❌ Main application is not accessible: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
# Check if API is responding
Write-Host "🔌 Checking API endpoints..." -ForegroundColor Yellow
try {
$response = Invoke-WebRequest -Uri "http://localhost:3000/api/tunnels" -UseBasicParsing -TimeoutSec 5
if ($response.StatusCode -eq 200) {
Write-Host "✅ API is responding" -ForegroundColor Green
} else {
Write-Host "❌ API returned status code: $($response.StatusCode)" -ForegroundColor Red
exit 1
}
} catch {
Write-Host "❌ API is not responding: $($_.Exception.Message)" -ForegroundColor Red
exit 1
}
# Check if FRPC container is running (if Docker is available)
if (Get-Command docker -ErrorAction SilentlyContinue) {
Write-Host "🐳 Checking FRPC container..." -ForegroundColor Yellow
$dockerPs = docker ps 2>$null
if ($dockerPs -and ($dockerPs | Select-String "frpc")) {
Write-Host "✅ FRPC container is running" -ForegroundColor Green
} else {
Write-Host "⚠️ FRPC container is not running (this is expected in development)" -ForegroundColor Yellow
}
} else {
Write-Host "⚠️ Docker is not available, skipping FRPC container check" -ForegroundColor Yellow
}
# Check if database is accessible
Write-Host "🗄️ Checking database..." -ForegroundColor Yellow
if (Test-Path "./data/tunnels.db") {
Write-Host "✅ Database file exists" -ForegroundColor Green
} else {
Write-Host " Database file doesn't exist yet (will be created on first use)" -ForegroundColor Blue
}
# Check if configuration directory exists
Write-Host "📁 Checking configuration directory..." -ForegroundColor Yellow
if (Test-Path "./data") {
Write-Host "✅ Configuration directory exists" -ForegroundColor Green
} else {
Write-Host "❌ Configuration directory is missing" -ForegroundColor Red
exit 1
}
Write-Host ""
Write-Host "🎉 Health check completed successfully!" -ForegroundColor Green
Write-Host "👉 Open http://localhost:3000 to access the FRP Manager" -ForegroundColor Cyan

58
app/health-check.sh Normal file
View file

@ -0,0 +1,58 @@
#!/bin/bash
# Health check script for FRP Manager
# This script checks if all services are running correctly
echo "🔍 FRP Manager Health Check"
echo "=========================="
# Check if main application is running
echo "📡 Checking main application..."
if curl -s http://localhost:3000/health > /dev/null; then
echo "✅ Main application is running"
else
echo "❌ Main application is not accessible"
exit 1
fi
# Check if API is responding
echo "🔌 Checking API endpoints..."
if curl -s http://localhost:3000/api/tunnels > /dev/null; then
echo "✅ API is responding"
else
echo "❌ API is not responding"
exit 1
fi
# Check if FRPC container is running (if Docker is available)
if command -v docker &> /dev/null; then
echo "🐳 Checking FRPC container..."
if docker ps | grep -q frpc; then
echo "✅ FRPC container is running"
else
echo "⚠️ FRPC container is not running (this is expected in development)"
fi
else
echo "⚠️ Docker is not available, skipping FRPC container check"
fi
# Check if database is accessible
echo "🗄️ Checking database..."
if [ -f "./data/tunnels.db" ]; then
echo "✅ Database file exists"
else
echo " Database file doesn't exist yet (will be created on first use)"
fi
# Check if configuration directory exists
echo "📁 Checking configuration directory..."
if [ -d "./data" ]; then
echo "✅ Configuration directory exists"
else
echo "❌ Configuration directory is missing"
exit 1
fi
echo ""
echo "🎉 Health check completed successfully!"
echo "👉 Open http://localhost:3000 to access the FRP Manager"

13
app/index.html Normal file
View file

@ -0,0 +1,13 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + React + TS</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/client/main.tsx"></script>
</body>
</html>

44
app/package.json Normal file
View file

@ -0,0 +1,44 @@
{
"name": "arc-frp",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "nodemon -w src/server -x tsx src/server/main.ts",
"start": "cross-env NODE_ENV=production tsx src/server/main.ts",
"build": "vite build"
},
"dependencies": {
"express": "^5.1.0",
"react": "^19.1.0",
"react-dom": "^19.1.0",
"tsx": "^4.19.3",
"typescript": "^5.8.3",
"vite-express": "*",
"cross-env": "^7.0.3",
"cors": "^2.8.5",
"sqlite3": "^5.1.6",
"better-sqlite3": "^9.2.2",
"zod": "^3.22.4",
"winston": "^3.11.0",
"uuid": "^9.0.1",
"react-router-dom": "^6.20.1",
"@tanstack/react-query": "^5.13.4",
"@tanstack/react-query-devtools": "^5.13.5",
"react-hot-toast": "^2.4.1",
"axios": "^1.6.2",
"lucide-react": "^0.294.0"
},
"devDependencies": {
"@types/express": "^5.0.1",
"@types/node": "^22.15.2",
"@types/react": "^19.1.2",
"@types/react-dom": "^19.1.2",
"@vitejs/plugin-react": "^4.4.1",
"nodemon": "^3.1.10",
"vite": "^6.3.3",
"@types/cors": "^2.8.17",
"@types/better-sqlite3": "^7.6.8",
"@types/uuid": "^9.0.7"
}
}

1
app/public/vite.svg Normal file
View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="31.88" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 257"><defs><linearGradient id="IconifyId1813088fe1fbc01fb466" x1="-.828%" x2="57.636%" y1="7.652%" y2="78.411%"><stop offset="0%" stop-color="#41D1FF"></stop><stop offset="100%" stop-color="#BD34FE"></stop></linearGradient><linearGradient id="IconifyId1813088fe1fbc01fb467" x1="43.376%" x2="50.316%" y1="2.242%" y2="89.03%"><stop offset="0%" stop-color="#FFEA83"></stop><stop offset="8.333%" stop-color="#FFDD35"></stop><stop offset="100%" stop-color="#FFA800"></stop></linearGradient></defs><path fill="url(#IconifyId1813088fe1fbc01fb466)" d="M255.153 37.938L134.897 252.976c-2.483 4.44-8.862 4.466-11.382.048L.875 37.958c-2.746-4.814 1.371-10.646 6.827-9.67l120.385 21.517a6.537 6.537 0 0 0 2.322-.004l117.867-21.483c5.438-.991 9.574 4.796 6.877 9.62Z"></path><path fill="url(#IconifyId1813088fe1fbc01fb467)" d="M185.432.063L96.44 17.501a3.268 3.268 0 0 0-2.634 3.014l-5.474 92.456a3.268 3.268 0 0 0 3.997 3.378l24.777-5.718c2.318-.535 4.413 1.507 3.936 3.838l-7.361 36.047c-.495 2.426 1.782 4.5 4.151 3.78l15.304-4.649c2.372-.72 4.652 1.36 4.15 3.788l-11.698 56.621c-.732 3.542 3.979 5.473 5.943 2.437l1.313-2.028l72.516-144.72c1.215-2.423-.88-5.186-3.54-4.672l-25.505 4.922c-2.396.462-4.435-1.77-3.759-4.114l16.646-57.705c.677-2.35-1.37-4.583-3.769-4.113Z"></path></svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

922
app/src/client/App.css Normal file
View file

@ -0,0 +1,922 @@
/* Base styles */
:root {
--primary: #0066cc;
--primary-hover: #0052a3;
--secondary: #6c757d;
--success: #28a745;
--danger: #dc3545;
--warning: #ffc107;
--info: #17a2b8;
--light: #f8f9fa;
--dark: #343a40;
--border: #dee2e6;
--bg-primary: #ffffff;
--bg-secondary: #f8f9fa;
--text-primary: #212529;
--text-secondary: #6c757d;
--shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
--shadow-lg: 0 4px 8px rgba(0, 0, 0, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen',
'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue',
sans-serif;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
background-color: var(--bg-secondary);
color: var(--text-primary);
}
.App {
min-height: 100vh;
display: flex;
flex-direction: column;
}
/* Navigation */
.navbar {
background-color: var(--bg-primary);
border-bottom: 1px solid var(--border);
padding: 1rem 2rem;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: var(--shadow);
}
.navbar-brand {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
color: var(--primary);
}
.navbar-icon {
color: var(--primary);
}
.navbar-nav {
display: flex;
gap: 1rem;
}
.nav-link {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
text-decoration: none;
color: var(--text-secondary);
transition: all 0.2s;
}
.nav-link:hover,
.nav-link.active {
background-color: var(--primary);
color: white;
}
/* Main content */
.main-content {
flex: 1;
padding: 2rem;
}
/* Buttons */
.btn {
display: inline-flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border: 1px solid transparent;
border-radius: 0.375rem;
font-size: 0.875rem;
font-weight: 500;
text-decoration: none;
cursor: pointer;
transition: all 0.2s;
background-color: transparent;
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.btn-primary {
background-color: var(--primary);
border-color: var(--primary);
color: white;
}
.btn-primary:hover:not(:disabled) {
background-color: var(--primary-hover);
border-color: var(--primary-hover);
}
.btn-secondary {
background-color: var(--secondary);
border-color: var(--secondary);
color: white;
}
.btn-success {
background-color: var(--success);
border-color: var(--success);
color: white;
}
.btn-danger {
background-color: var(--danger);
border-color: var(--danger);
color: white;
}
.btn-warning {
background-color: var(--warning);
border-color: var(--warning);
color: var(--dark);
}
.btn-ghost {
background-color: transparent;
border-color: transparent;
color: var(--text-secondary);
}
.btn-ghost:hover:not(:disabled) {
background-color: var(--bg-secondary);
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
}
/* Dashboard */
.dashboard {
max-width: 1200px;
margin: 0 auto;
}
.dashboard-header {
margin-bottom: 2rem;
}
.dashboard-header h1 {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.5rem;
}
.dashboard-header p {
font-size: 1.125rem;
color: var(--text-secondary);
}
.stats-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stat-card {
background-color: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
display: flex;
align-items: center;
gap: 1rem;
box-shadow: var(--shadow);
}
.stat-icon {
flex-shrink: 0;
}
.stat-content h3 {
font-size: 0.875rem;
font-weight: 600;
color: var(--text-secondary);
margin-bottom: 0.25rem;
}
.stat-number {
font-size: 2rem;
font-weight: 700;
color: var(--text-primary);
margin-bottom: 0.25rem;
}
.stat-description {
font-size: 0.75rem;
color: var(--text-secondary);
}
.dashboard-sections {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
}
.section {
background-color: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: var(--shadow);
}
.section h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.tunnel-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.tunnel-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background-color: var(--bg-secondary);
}
.tunnel-info h4 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.tunnel-info p {
font-size: 0.875rem;
color: var(--text-secondary);
}
.status-list {
display: flex;
flex-direction: column;
gap: 1rem;
}
.status-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 1rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background-color: var(--bg-secondary);
}
.status-indicator {
flex-shrink: 0;
}
.status-content h4 {
font-size: 1rem;
font-weight: 600;
margin-bottom: 0.25rem;
}
.status-content p {
font-size: 0.875rem;
color: var(--text-secondary);
}
/* Tunnel Manager */
.tunnel-manager {
max-width: 1200px;
margin: 0 auto;
}
.tunnel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.tunnel-header h1 {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
}
.header-actions {
display: flex;
align-items: center;
gap: 1rem;
}
.node-status {
display: flex;
align-items: center;
gap: 0.5rem;
}
.node-indicator {
padding: 0.25rem 0.75rem;
border-radius: 1rem;
font-size: 0.875rem;
font-weight: 500;
}
.node-indicator.online {
background-color: #d4edda;
color: #155724;
border: 1px solid #c3e6cb;
}
.node-indicator.offline {
background-color: #f8d7da;
color: #721c24;
border: 1px solid #f5c6cb;
}
.spinning {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.tunnels-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(400px, 1fr));
gap: 1.5rem;
}
.tunnel-card {
background-color: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: var(--shadow);
}
.tunnel-card-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 1rem;
}
.tunnel-title h3 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 0.5rem;
}
.tunnel-badges {
display: flex;
gap: 0.5rem;
flex-wrap: wrap;
}
.tunnel-actions {
display: flex;
gap: 0.5rem;
flex-shrink: 0;
}
.tunnel-info {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.info-label {
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 500;
}
.info-value {
font-size: 0.875rem;
color: var(--text-primary);
font-family: monospace;
}
.tunnel-error {
margin-top: 1rem;
padding: 0.75rem;
background-color: #fef2f2;
border: 1px solid #fecaca;
border-radius: 0.375rem;
}
.error-text {
font-size: 0.875rem;
color: #dc2626;
}
/* Badges */
.badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.badge-success {
background-color: #d1fae5;
color: #065f46;
}
.badge-secondary {
background-color: #f3f4f6;
color: #374151;
}
.badge-active {
background-color: #dbeafe;
color: #1e40af;
}
.badge-inactive {
background-color: #fef3c7;
color: #92400e;
}
/* Status badges */
.status-badge {
display: inline-block;
padding: 0.25rem 0.5rem;
font-size: 0.75rem;
font-weight: 500;
border-radius: 0.25rem;
text-transform: uppercase;
letter-spacing: 0.025em;
}
.status-active {
background-color: #d1fae5;
color: #065f46;
}
.status-inactive {
background-color: #fef3c7;
color: #92400e;
}
/* Empty state */
.empty-state {
text-align: center;
padding: 3rem;
color: var(--text-secondary);
}
.empty-icon {
margin-bottom: 1rem;
opacity: 0.5;
}
.empty-state h3 {
font-size: 1.5rem;
margin-bottom: 0.5rem;
}
.empty-state p {
margin-bottom: 1.5rem;
}
/* Form overlay */
.form-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background-color: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.form-modal {
background-color: var(--bg-primary);
border-radius: 0.5rem;
max-width: 600px;
width: 90%;
max-height: 90vh;
overflow-y: auto;
box-shadow: var(--shadow-lg);
}
.form-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1.5rem;
border-bottom: 1px solid var(--border);
}
.form-header h2 {
font-size: 1.5rem;
font-weight: 600;
}
.tunnel-form {
padding: 1.5rem;
}
.form-group {
margin-bottom: 1rem;
}
.form-group label {
display: block;
font-size: 0.875rem;
font-weight: 500;
margin-bottom: 0.25rem;
color: var(--text-primary);
}
.form-group input,
.form-group select {
width: 100%;
padding: 0.5rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font-size: 0.875rem;
transition: border-color 0.2s;
}
.form-group input:focus,
.form-group select:focus {
outline: none;
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(0, 102, 204, 0.1);
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 1rem;
}
.checkbox-label {
display: flex;
align-items: center;
gap: 0.5rem;
cursor: pointer;
}
.checkbox-label input[type="checkbox"] {
width: auto;
}
.form-actions {
display: flex;
justify-content: flex-end;
gap: 1rem;
margin-top: 1.5rem;
padding-top: 1.5rem;
border-top: 1px solid var(--border);
}
/* Server Status */
.server-status {
max-width: 1200px;
margin: 0 auto;
}
.server-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 2rem;
}
.server-header h1 {
font-size: 2.5rem;
font-weight: 700;
color: var(--text-primary);
}
.server-info {
display: flex;
align-items: center;
gap: 1rem;
}
.status-indicator {
display: flex;
align-items: center;
gap: 0.5rem;
padding: 0.5rem 1rem;
border-radius: 0.375rem;
background-color: var(--bg-primary);
border: 1px solid var(--border);
}
.status-text {
font-size: 1rem;
font-weight: 600;
}
.server-controls {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 2rem;
margin-bottom: 2rem;
}
.control-section {
background-color: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: var(--shadow);
}
.control-section h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.control-buttons {
display: flex;
gap: 1rem;
flex-wrap: wrap;
}
.status-section {
background-color: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: var(--shadow);
}
.status-section h2 {
font-size: 1.25rem;
font-weight: 600;
margin-bottom: 1rem;
}
.info-grid {
display: flex;
flex-direction: column;
gap: 1rem;
}
.info-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
background-color: var(--bg-secondary);
}
.info-label {
font-size: 0.875rem;
color: var(--text-secondary);
font-weight: 500;
}
.info-value {
font-size: 0.875rem;
color: var(--text-primary);
font-family: monospace;
}
/* Logs */
.logs-section {
background-color: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: var(--shadow);
margin-bottom: 2rem;
}
.logs-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 1rem;
}
.logs-header h2 {
display: flex;
align-items: center;
gap: 0.5rem;
font-size: 1.25rem;
font-weight: 600;
}
.logs-controls {
display: flex;
gap: 1rem;
align-items: center;
}
.logs-select {
padding: 0.375rem 0.75rem;
border: 1px solid var(--border);
border-radius: 0.375rem;
font-size: 0.875rem;
}
.logs-container {
background-color: var(--dark);
color: var(--light);
border-radius: 0.375rem;
padding: 1rem;
font-family: 'Courier New', monospace;
font-size: 0.875rem;
line-height: 1.4;
max-height: 400px;
overflow-y: auto;
}
.logs-content {
white-space: pre-wrap;
word-wrap: break-word;
}
.logs-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 2rem;
color: var(--text-secondary);
}
/* System info */
.system-info {
margin-bottom: 2rem;
}
.system-info h2 {
font-size: 1.5rem;
font-weight: 600;
margin-bottom: 1rem;
}
.info-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1.5rem;
}
.info-card {
background-color: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 0.5rem;
padding: 1.5rem;
box-shadow: var(--shadow);
}
.info-card-header {
display: flex;
align-items: center;
gap: 1rem;
margin-bottom: 1rem;
}
.info-card-header h3 {
font-size: 1.25rem;
font-weight: 600;
}
.info-card-content p {
margin-bottom: 0.5rem;
color: var(--text-secondary);
}
.info-detail {
font-size: 0.875rem;
font-weight: 500;
}
/* Utility classes */
.loading {
display: flex;
align-items: center;
justify-content: center;
gap: 0.5rem;
padding: 2rem;
color: var(--text-secondary);
}
.animate-spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.text-green-500 {
color: #10b981;
}
.text-red-500 {
color: #ef4444;
}
.text-yellow-500 {
color: #f59e0b;
}
.text-blue-500 {
color: #3b82f6;
}
/* Responsive design */
@media (max-width: 768px) {
.navbar {
flex-direction: column;
gap: 1rem;
}
.navbar-nav {
justify-content: center;
}
.main-content {
padding: 1rem;
}
.dashboard-sections {
grid-template-columns: 1fr;
}
.server-controls {
grid-template-columns: 1fr;
}
.control-buttons {
flex-direction: column;
}
.tunnels-grid {
grid-template-columns: 1fr;
}
.form-modal {
width: 95%;
}
.form-row {
grid-template-columns: 1fr;
}
.tunnel-header {
flex-direction: column;
gap: 1rem;
align-items: flex-start;
}
.stats-grid {
grid-template-columns: 1fr;
}
}

51
app/src/client/App.tsx Normal file
View file

@ -0,0 +1,51 @@
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';
import { Toaster } from 'react-hot-toast';
import Navbar from './components/Navbar';
import Dashboard from './pages/Dashboard';
import TunnelManager from './pages/TunnelManager';
import ServerStatus from './pages/ServerStatus';
import './App.css';
const queryClient = new QueryClient({
defaultOptions: {
queries: {
retry: 2,
staleTime: 5 * 1000, // 5 seconds
},
},
});
function App() {
return (
<QueryClientProvider client={queryClient}>
<Router>
<div className="App">
<Navbar />
<main className="main-content">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/tunnels" element={<TunnelManager />} />
<Route path="/status" element={<ServerStatus />} />
</Routes>
</main>
<Toaster
position="top-right"
toastOptions={{
duration: 4000,
style: {
background: '#363636',
color: '#fff',
},
}}
/>
</div>
</Router>
<ReactQueryDevtools initialIsOpen={false} />
</QueryClientProvider>
);
}
export default App;

View file

@ -0,0 +1,199 @@
import axios from 'axios';
export interface TunnelConfig {
id?: string;
name: string;
protocol: 'TCP' | 'UDP';
localIp: string;
localPort: number;
remotePort: number;
enabled: boolean;
createdAt?: string;
updatedAt?: string;
}
export interface TunnelStatus {
id: string;
name: string;
active: boolean;
lastChecked: string;
error?: string;
}
export interface FrpcStatus {
running: boolean;
}
export interface FrpcLogs {
logs: string;
}
export interface NodeStatus {
status: string;
timestamp: string;
uptime?: number;
memory?: {
used: number;
total: number;
};
cpu?: {
usage: number;
};
connection?: {
url: string;
isOnline: boolean;
lastConnectionTime: Date | null;
};
}
export interface NodeConnection {
url: string;
isOnline: boolean;
lastConnectionTime: Date | null;
isReachable: boolean;
}
const API_BASE_URL = '/api';
const apiClient = axios.create({
baseURL: API_BASE_URL,
timeout: 10000,
});
// Request interceptor for logging
apiClient.interceptors.request.use(
(config) => {
console.log(`API Request: ${config.method?.toUpperCase()} ${config.url}`);
return config;
},
(error) => {
console.error('API Request Error:', error);
return Promise.reject(error);
}
);
// Response interceptor for error handling
apiClient.interceptors.response.use(
(response) => response,
(error) => {
console.error('API Response Error:', error);
return Promise.reject(error);
}
);
export const tunnelApi = {
// Get all tunnels
getAllTunnels: async (): Promise<TunnelConfig[]> => {
const response = await apiClient.get('/tunnels');
return response.data;
},
// Get tunnel by ID
getTunnelById: async (id: string): Promise<TunnelConfig> => {
const response = await apiClient.get(`/tunnels/${id}`);
return response.data;
},
// Create new tunnel
createTunnel: async (tunnel: Omit<TunnelConfig, 'id' | 'createdAt' | 'updatedAt'>): Promise<TunnelConfig> => {
const response = await apiClient.post('/tunnels', tunnel);
return response.data;
},
// Update tunnel
updateTunnel: async (id: string, tunnel: Partial<TunnelConfig>): Promise<TunnelConfig> => {
const response = await apiClient.put(`/tunnels/${id}`, tunnel);
return response.data;
},
// Delete tunnel
deleteTunnel: async (id: string): Promise<void> => {
await apiClient.delete(`/tunnels/${id}`);
},
// Get tunnel status
getTunnelStatus: async (id: string): Promise<TunnelStatus> => {
const response = await apiClient.get(`/tunnels/${id}/status`);
return response.data;
},
// Get all tunnel statuses
getAllTunnelStatuses: async (): Promise<TunnelStatus[]> => {
const response = await apiClient.get('/tunnels-status');
return response.data;
},
};
export const frpcApi = {
// Get frpc status
getStatus: async (): Promise<FrpcStatus> => {
const response = await apiClient.get('/frpc/status');
return response.data;
},
// Control frpc service
start: async (): Promise<{ message: string }> => {
const response = await apiClient.post('/frpc/start');
return response.data;
},
stop: async (): Promise<{ message: string }> => {
const response = await apiClient.post('/frpc/stop');
return response.data;
},
restart: async (): Promise<{ message: string }> => {
const response = await apiClient.post('/frpc/restart');
return response.data;
},
regenerate: async (): Promise<{ message: string }> => {
const response = await apiClient.post('/frpc/regenerate');
return response.data;
},
// Get frpc logs
getLogs: async (lines: number = 50): Promise<FrpcLogs> => {
const response = await apiClient.get(`/frpc/logs?lines=${lines}`);
return response.data;
},
};
export const nodeApi = {
// Get node status
getStatus: async (): Promise<NodeStatus> => {
const response = await apiClient.get('/node/status');
return response.data;
},
// Get node connection info
getConnection: async (): Promise<NodeConnection> => {
const response = await apiClient.get('/node/connection');
return response.data;
},
// Push configuration to node
pushConfig: async (): Promise<{ message: string; tunnelCount: number; nodeResponse: any }> => {
const response = await apiClient.post('/node/push-config');
return response.data;
},
// Restart frpc on node
restartFrpc: async (): Promise<{ message: string; nodeResponse: any }> => {
const response = await apiClient.post('/node/restart-frpc');
return response.data;
},
// Push config and restart frpc on node
pushAndRestart: async (): Promise<{
message: string;
tunnelCount: number;
configResponse: any;
restartResponse: any
}> => {
const response = await apiClient.post('/node/push-and-restart');
return response.data;
},
};
export default apiClient;

View file

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" class="iconify iconify--logos" width="35.93" height="32" preserveAspectRatio="xMidYMid meet" viewBox="0 0 256 228"><path fill="#00D8FF" d="M210.483 73.824a171.49 171.49 0 0 0-8.24-2.597c.465-1.9.893-3.777 1.273-5.621c6.238-30.281 2.16-54.676-11.769-62.708c-13.355-7.7-35.196.329-57.254 19.526a171.23 171.23 0 0 0-6.375 5.848a155.866 155.866 0 0 0-4.241-3.917C100.759 3.829 77.587-4.822 63.673 3.233C50.33 10.957 46.379 33.89 51.995 62.588a170.974 170.974 0 0 0 1.892 8.48c-3.28.932-6.445 1.924-9.474 2.98C17.309 83.498 0 98.307 0 113.668c0 15.865 18.582 31.778 46.812 41.427a145.52 145.52 0 0 0 6.921 2.165a167.467 167.467 0 0 0-2.01 9.138c-5.354 28.2-1.173 50.591 12.134 58.266c13.744 7.926 36.812-.22 59.273-19.855a145.567 145.567 0 0 0 5.342-4.923a168.064 168.064 0 0 0 6.92 6.314c21.758 18.722 43.246 26.282 56.54 18.586c13.731-7.949 18.194-32.003 12.4-61.268a145.016 145.016 0 0 0-1.535-6.842c1.62-.48 3.21-.974 4.76-1.488c29.348-9.723 48.443-25.443 48.443-41.52c0-15.417-17.868-30.326-45.517-39.844Zm-6.365 70.984c-1.4.463-2.836.91-4.3 1.345c-3.24-10.257-7.612-21.163-12.963-32.432c5.106-11 9.31-21.767 12.459-31.957c2.619.758 5.16 1.557 7.61 2.4c23.69 8.156 38.14 20.213 38.14 29.504c0 9.896-15.606 22.743-40.946 31.14Zm-10.514 20.834c2.562 12.94 2.927 24.64 1.23 33.787c-1.524 8.219-4.59 13.698-8.382 15.893c-8.067 4.67-25.32-1.4-43.927-17.412a156.726 156.726 0 0 1-6.437-5.87c7.214-7.889 14.423-17.06 21.459-27.246c12.376-1.098 24.068-2.894 34.671-5.345a134.17 134.17 0 0 1 1.386 6.193ZM87.276 214.515c-7.882 2.783-14.16 2.863-17.955.675c-8.075-4.657-11.432-22.636-6.853-46.752a156.923 156.923 0 0 1 1.869-8.499c10.486 2.32 22.093 3.988 34.498 4.994c7.084 9.967 14.501 19.128 21.976 27.15a134.668 134.668 0 0 1-4.877 4.492c-9.933 8.682-19.886 14.842-28.658 17.94ZM50.35 144.747c-12.483-4.267-22.792-9.812-29.858-15.863c-6.35-5.437-9.555-10.836-9.555-15.216c0-9.322 13.897-21.212 37.076-29.293c2.813-.98 5.757-1.905 8.812-2.773c3.204 10.42 7.406 21.315 12.477 32.332c-5.137 11.18-9.399 22.249-12.634 32.792a134.718 134.718 0 0 1-6.318-1.979Zm12.378-84.26c-4.811-24.587-1.616-43.134 6.425-47.789c8.564-4.958 27.502 2.111 47.463 19.835a144.318 144.318 0 0 1 3.841 3.545c-7.438 7.987-14.787 17.08-21.808 26.988c-12.04 1.116-23.565 2.908-34.161 5.309a160.342 160.342 0 0 1-1.76-7.887Zm110.427 27.268a347.8 347.8 0 0 0-7.785-12.803c8.168 1.033 15.994 2.404 23.343 4.08c-2.206 7.072-4.956 14.465-8.193 22.045a381.151 381.151 0 0 0-7.365-13.322Zm-45.032-43.861c5.044 5.465 10.096 11.566 15.065 18.186a322.04 322.04 0 0 0-30.257-.006c4.974-6.559 10.069-12.652 15.192-18.18ZM82.802 87.83a323.167 323.167 0 0 0-7.227 13.238c-3.184-7.553-5.909-14.98-8.134-22.152c7.304-1.634 15.093-2.97 23.209-3.984a321.524 321.524 0 0 0-7.848 12.897Zm8.081 65.352c-8.385-.936-16.291-2.203-23.593-3.793c2.26-7.3 5.045-14.885 8.298-22.6a321.187 321.187 0 0 0 7.257 13.246c2.594 4.48 5.28 8.868 8.038 13.147Zm37.542 31.03c-5.184-5.592-10.354-11.779-15.403-18.433c4.902.192 9.899.29 14.978.29c5.218 0 10.376-.117 15.453-.343c-4.985 6.774-10.018 12.97-15.028 18.486Zm52.198-57.817c3.422 7.8 6.306 15.345 8.596 22.52c-7.422 1.694-15.436 3.058-23.88 4.071a382.417 382.417 0 0 0 7.859-13.026a347.403 347.403 0 0 0 7.425-13.565Zm-16.898 8.101a358.557 358.557 0 0 1-12.281 19.815a329.4 329.4 0 0 1-23.444.823c-7.967 0-15.716-.248-23.178-.732a310.202 310.202 0 0 1-12.513-19.846h.001a307.41 307.41 0 0 1-10.923-20.627a310.278 310.278 0 0 1 10.89-20.637l-.001.001a307.318 307.318 0 0 1 12.413-19.761c7.613-.576 15.42-.876 23.31-.876H128c7.926 0 15.743.303 23.354.883a329.357 329.357 0 0 1 12.335 19.695a358.489 358.489 0 0 1 11.036 20.54a329.472 329.472 0 0 1-11 20.722Zm22.56-122.124c8.572 4.944 11.906 24.881 6.52 51.026c-.344 1.668-.73 3.367-1.15 5.09c-10.622-2.452-22.155-4.275-34.23-5.408c-7.034-10.017-14.323-19.124-21.64-27.008a160.789 160.789 0 0 1 5.888-5.4c18.9-16.447 36.564-22.941 44.612-18.3ZM128 90.808c12.625 0 22.86 10.235 22.86 22.86s-10.235 22.86-22.86 22.86s-22.86-10.235-22.86-22.86s10.235-22.86 22.86-22.86Z"></path></svg>

After

Width:  |  Height:  |  Size: 4 KiB

View file

@ -0,0 +1,43 @@
import React from 'react';
import { Link, useLocation } from 'react-router-dom';
import { Server, Settings, Activity } from 'lucide-react';
const Navbar: React.FC = () => {
const location = useLocation();
const isActive = (path: string) => location.pathname === path;
return (
<nav className="navbar">
<div className="navbar-brand">
<Server className="navbar-icon" />
<span>FRP Manager</span>
</div>
<div className="navbar-nav">
<Link
to="/"
className={`nav-link ${isActive('/') ? 'active' : ''}`}
>
<Activity size={18} />
Dashboard
</Link>
<Link
to="/tunnels"
className={`nav-link ${isActive('/tunnels') ? 'active' : ''}`}
>
<Settings size={18} />
Tunnels
</Link>
<Link
to="/status"
className={`nav-link ${isActive('/status') ? 'active' : ''}`}
>
<Server size={18} />
Server Status
</Link>
</div>
</nav>
);
};
export default Navbar;

View file

@ -0,0 +1,232 @@
import React, { useState } from 'react';
import { useMutation, useQueryClient } from '@tanstack/react-query';
import { tunnelApi, TunnelConfig } from '../api/client';
import { X, Save, Loader2 } from 'lucide-react';
import { toast } from 'react-hot-toast';
interface TunnelFormProps {
tunnel?: TunnelConfig | null;
onClose: () => void;
onSubmit: () => void;
}
const TunnelForm: React.FC<TunnelFormProps> = ({ tunnel, onClose, onSubmit }) => {
const queryClient = useQueryClient();
const isEditing = Boolean(tunnel);
const [formData, setFormData] = useState({
name: tunnel?.name || '',
protocol: tunnel?.protocol || 'TCP' as 'TCP' | 'UDP',
localIp: tunnel?.localIp || '127.0.0.1',
localPort: tunnel?.localPort || 8080,
remotePort: tunnel?.remotePort || 8080,
enabled: tunnel?.enabled ?? true,
});
const createMutation = useMutation({
mutationFn: tunnelApi.createTunnel,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
toast.success('Tunnel created successfully');
onSubmit();
},
onError: (error) => {
console.error('Create error:', error);
toast.error('Failed to create tunnel');
},
});
const updateMutation = useMutation({
mutationFn: ({ id, updates }: { id: string; updates: Partial<TunnelConfig> }) =>
tunnelApi.updateTunnel(id, updates),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
toast.success('Tunnel updated successfully');
onSubmit();
},
onError: (error) => {
console.error('Update error:', error);
toast.error('Failed to update tunnel');
},
});
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!formData.name.trim()) {
toast.error('Please enter a name');
return;
}
if (formData.localPort < 1 || formData.localPort > 65535) {
toast.error('Local port must be between 1 and 65535');
return;
}
if (formData.remotePort < 1 || formData.remotePort > 65535) {
toast.error('Remote port must be between 1 and 65535');
return;
}
if (isEditing && tunnel) {
updateMutation.mutate({
id: tunnel.id!,
updates: formData,
});
} else {
createMutation.mutate(formData);
}
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement | HTMLSelectElement>) => {
const { name, value, type } = e.target;
if (type === 'checkbox') {
const checked = (e.target as HTMLInputElement).checked;
setFormData(prev => ({
...prev,
[name]: checked,
}));
} else if (type === 'number') {
setFormData(prev => ({
...prev,
[name]: parseInt(value) || 0,
}));
} else {
setFormData(prev => ({
...prev,
[name]: value,
}));
}
};
const isPending = createMutation.isPending || updateMutation.isPending;
return (
<div className="form-overlay">
<div className="form-modal">
<div className="form-header">
<h2>{isEditing ? 'Edit Tunnel' : 'Create New Tunnel'}</h2>
<button
className="btn btn-ghost btn-sm"
onClick={onClose}
disabled={isPending}
>
<X size={18} />
</button>
</div>
<form onSubmit={handleSubmit} className="tunnel-form">
<div className="form-group">
<label htmlFor="name">Name</label>
<input
type="text"
id="name"
name="name"
value={formData.name}
onChange={handleChange}
placeholder="e.g., Minecraft Server"
required
disabled={isPending}
/>
</div>
<div className="form-group">
<label htmlFor="protocol">Protocol</label>
<select
id="protocol"
name="protocol"
value={formData.protocol}
onChange={handleChange}
disabled={isPending}
>
<option value="TCP">TCP</option>
<option value="UDP">UDP</option>
</select>
</div>
<div className="form-row">
<div className="form-group">
<label htmlFor="localIp">Local IP</label>
<input
type="text"
id="localIp"
name="localIp"
value={formData.localIp}
onChange={handleChange}
placeholder="127.0.0.1"
required
disabled={isPending}
/>
</div>
<div className="form-group">
<label htmlFor="localPort">Local Port</label>
<input
type="number"
id="localPort"
name="localPort"
value={formData.localPort}
onChange={handleChange}
min="1"
max="65535"
required
disabled={isPending}
/>
</div>
</div>
<div className="form-group">
<label htmlFor="remotePort">Remote Port</label>
<input
type="number"
id="remotePort"
name="remotePort"
value={formData.remotePort}
onChange={handleChange}
min="1"
max="65535"
required
disabled={isPending}
/>
</div>
<div className="form-group">
<label className="checkbox-label">
<input
type="checkbox"
name="enabled"
checked={formData.enabled}
onChange={handleChange}
disabled={isPending}
/>
<span>Enable this tunnel</span>
</label>
</div>
<div className="form-actions">
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
disabled={isPending}
>
Cancel
</button>
<button
type="submit"
className="btn btn-primary"
disabled={isPending}
>
{isPending && <Loader2 className="animate-spin" size={16} />}
<Save size={16} />
{isEditing ? 'Update' : 'Create'}
</button>
</div>
</form>
</div>
</div>
);
};
export default TunnelForm;

69
app/src/client/index.css Normal file
View file

@ -0,0 +1,69 @@
:root {
font-family: Inter, system-ui, Avenir, Helvetica, Arial, sans-serif;
line-height: 1.5;
font-weight: 400;
color-scheme: light dark;
color: rgba(255, 255, 255, 0.87);
background-color: #242424;
font-synthesis: none;
text-rendering: optimizeLegibility;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
-webkit-text-size-adjust: 100%;
}
a {
font-weight: 500;
color: #646cff;
text-decoration: inherit;
}
a:hover {
color: #535bf2;
}
body {
margin: 0;
display: flex;
place-items: center;
min-width: 320px;
min-height: 100vh;
}
h1 {
font-size: 3.2em;
line-height: 1.1;
}
button {
border-radius: 8px;
border: 1px solid transparent;
padding: 0.6em 1.2em;
font-size: 1em;
font-weight: 500;
font-family: inherit;
background-color: #1a1a1a;
cursor: pointer;
transition: border-color 0.25s;
}
button:hover {
border-color: #646cff;
}
button:focus,
button:focus-visible {
outline: 4px auto -webkit-focus-ring-color;
}
@media (prefers-color-scheme: light) {
:root {
color: #213547;
background-color: #ffffff;
}
a:hover {
color: #747bff;
}
button {
background-color: #f9f9f9;
}
}

12
app/src/client/main.tsx Normal file
View file

@ -0,0 +1,12 @@
import "./index.css";
import React from "react";
import ReactDOM from "react-dom/client";
import App from "./App";
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<React.StrictMode>
<App />
</React.StrictMode>,
);

View file

@ -0,0 +1,213 @@
import React from 'react';
import { useQuery } from '@tanstack/react-query';
import { tunnelApi, frpcApi, nodeApi } from '../api/client';
import { CheckCircle, XCircle, AlertCircle, RefreshCw, Wifi, WifiOff } from 'lucide-react';
const Dashboard: React.FC = () => {
const { data: tunnels, isLoading: tunnelsLoading } = useQuery({
queryKey: ['tunnels'],
queryFn: tunnelApi.getAllTunnels,
refetchInterval: 5000,
});
const { data: tunnelStatuses, isLoading: statusLoading } = useQuery({
queryKey: ['tunnel-statuses'],
queryFn: tunnelApi.getAllTunnelStatuses,
refetchInterval: 10000,
});
const { data: frpcStatus, isLoading: frpcLoading } = useQuery({
queryKey: ['frpc-status'],
queryFn: frpcApi.getStatus,
refetchInterval: 5000,
});
const { data: nodeConnection } = useQuery({
queryKey: ['node-connection'],
queryFn: nodeApi.getConnection,
refetchInterval: 30000,
retry: false,
});
const { data: nodeStatus } = useQuery({
queryKey: ['node-status'],
queryFn: nodeApi.getStatus,
refetchInterval: 30000,
retry: false,
enabled: !!nodeConnection?.isReachable,
});
const activeTunnels = tunnels?.filter(t => t.enabled) || [];
const activeTunnelStatuses = tunnelStatuses?.filter(s => s.active) || [];
if (tunnelsLoading || statusLoading || frpcLoading) {
return (
<div className="dashboard">
<div className="loading">
<RefreshCw className="animate-spin" size={24} />
<span>Loading dashboard...</span>
</div>
</div>
);
}
return (
<div className="dashboard">
<div className="dashboard-header">
<h1>FRP Manager Dashboard</h1>
<p>Manage your tunnel configurations and monitor their status</p>
</div>
<div className="stats-grid">
<div className="stat-card">
<div className="stat-icon">
<CheckCircle className="text-green-500" size={24} />
</div>
<div className="stat-content">
<h3>Active Tunnels</h3>
<p className="stat-number">{activeTunnelStatuses.length}</p>
<p className="stat-description">Currently running</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<AlertCircle className="text-yellow-500" size={24} />
</div>
<div className="stat-content">
<h3>Total Tunnels</h3>
<p className="stat-number">{tunnels?.length || 0}</p>
<p className="stat-description">Configured tunnels</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
{frpcStatus?.running ? (
<CheckCircle className="text-green-500" size={24} />
) : (
<XCircle className="text-red-500" size={24} />
)}
</div>
<div className="stat-content">
<h3>FRPC Service</h3>
<p className="stat-number">{frpcStatus?.running ? 'Running' : 'Stopped'}</p>
<p className="stat-description">Service status</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
<CheckCircle className="text-blue-500" size={24} />
</div>
<div className="stat-content">
<h3>Enabled Tunnels</h3>
<p className="stat-number">{activeTunnels.length}</p>
<p className="stat-description">Ready to connect</p>
</div>
</div>
<div className="stat-card">
<div className="stat-icon">
{nodeConnection?.isReachable ? (
<Wifi className="text-green-500" size={24} />
) : (
<WifiOff className="text-red-500" size={24} />
)}
</div>
<div className="stat-content">
<h3>Node Status</h3>
<p className="stat-number">{nodeConnection?.isReachable ? 'Online' : 'Offline'}</p>
<p className="stat-description">Home server agent</p>
</div>
</div>
</div>
<div className="dashboard-sections">
<div className="section">
<h2>Recent Tunnels</h2>
<div className="tunnel-list">
{activeTunnels.slice(0, 5).map(tunnel => (
<div key={tunnel.id} className="tunnel-item">
<div className="tunnel-info">
<h4>{tunnel.name}</h4>
<p>{tunnel.protocol} {tunnel.localIp}:{tunnel.localPort} :{tunnel.remotePort}</p>
</div>
<div className="tunnel-status">
{tunnelStatuses?.find(s => s.id === tunnel.id)?.active ? (
<span className="status-badge status-active">Active</span>
) : (
<span className="status-badge status-inactive">Inactive</span>
)}
</div>
</div>
))}
{activeTunnels.length === 0 && (
<p className="empty-state">No active tunnels configured</p>
)}
</div>
</div>
<div className="section">
<h2>System Status</h2>
<div className="status-list">
<div className="status-item">
<div className="status-indicator">
{frpcStatus?.running ? (
<CheckCircle className="text-green-500" size={20} />
) : (
<XCircle className="text-red-500" size={20} />
)}
</div>
<div className="status-content">
<h4>FRPC Service</h4>
<p>{frpcStatus?.running ? 'Service is running normally' : 'Service is stopped'}</p>
</div>
</div>
<div className="status-item">
<div className="status-indicator">
<CheckCircle className="text-green-500" size={20} />
</div>
<div className="status-content">
<h4>API Server</h4>
<p>API is responding normally</p>
</div>
</div>
<div className="status-item">
<div className="status-indicator">
<CheckCircle className="text-green-500" size={20} />
</div>
<div className="status-content">
<h4>Database</h4>
<p>Database connection is healthy</p>
</div>
</div>
<div className="status-item">
<div className="status-indicator">
{nodeConnection?.isReachable ? (
<Wifi className="text-green-500" size={20} />
) : (
<WifiOff className="text-red-500" size={20} />
)}
</div>
<div className="status-content">
<h4>Home Server Node</h4>
<p>
{nodeConnection?.isReachable
? `Connected • Last seen: ${nodeConnection.lastConnectionTime ? new Date(nodeConnection.lastConnectionTime).toLocaleTimeString() : 'Now'}`
: 'Disconnected or unreachable'
}
</p>
</div>
</div>
</div>
</div>
</div>
</div>
);
};
export default Dashboard;

View file

@ -0,0 +1,253 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { frpcApi } from '../api/client';
import { Server, Play, Square, RotateCcw, RefreshCw, FileText, Activity } from 'lucide-react';
import { toast } from 'react-hot-toast';
const ServerStatus: React.FC = () => {
const [logsLines, setLogsLines] = useState(50);
const queryClient = useQueryClient();
const { data: frpcStatus, isLoading: statusLoading } = useQuery({
queryKey: ['frpc-status'],
queryFn: frpcApi.getStatus,
refetchInterval: 3000,
});
const { data: logs, isLoading: logsLoading } = useQuery({
queryKey: ['frpc-logs', logsLines],
queryFn: () => frpcApi.getLogs(logsLines),
refetchInterval: 5000,
});
const startMutation = useMutation({
mutationFn: frpcApi.start,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
toast.success('FRPC service started');
},
onError: (error) => {
console.error('Start error:', error);
toast.error('Failed to start FRPC service');
},
});
const stopMutation = useMutation({
mutationFn: frpcApi.stop,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
toast.success('FRPC service stopped');
},
onError: (error) => {
console.error('Stop error:', error);
toast.error('Failed to stop FRPC service');
},
});
const restartMutation = useMutation({
mutationFn: frpcApi.restart,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
toast.success('FRPC service restarted');
},
onError: (error) => {
console.error('Restart error:', error);
toast.error('Failed to restart FRPC service');
},
});
const regenerateMutation = useMutation({
mutationFn: frpcApi.regenerate,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['frpc-status'] });
toast.success('FRPC configuration regenerated');
},
onError: (error) => {
console.error('Regenerate error:', error);
toast.error('Failed to regenerate FRPC configuration');
},
});
const handleStart = () => startMutation.mutate();
const handleStop = () => stopMutation.mutate();
const handleRestart = () => restartMutation.mutate();
const handleRegenerate = () => regenerateMutation.mutate();
const isLoading = statusLoading || logsLoading;
const isAnyMutationPending =
startMutation.isPending ||
stopMutation.isPending ||
restartMutation.isPending ||
regenerateMutation.isPending;
return (
<div className="server-status">
<div className="server-header">
<h1>Server Status</h1>
<div className="server-info">
<div className="status-indicator">
<Activity
size={24}
className={frpcStatus?.running ? 'text-green-500' : 'text-red-500'}
/>
<span className={`status-text ${frpcStatus?.running ? 'text-green-500' : 'text-red-500'}`}>
{frpcStatus?.running ? 'Running' : 'Stopped'}
</span>
</div>
</div>
</div>
<div className="server-controls">
<div className="control-section">
<h2>Service Controls</h2>
<div className="control-buttons">
<button
className="btn btn-success"
onClick={handleStart}
disabled={isAnyMutationPending || frpcStatus?.running}
>
<Play size={18} />
Start
</button>
<button
className="btn btn-danger"
onClick={handleStop}
disabled={isAnyMutationPending || !frpcStatus?.running}
>
<Square size={18} />
Stop
</button>
<button
className="btn btn-warning"
onClick={handleRestart}
disabled={isAnyMutationPending}
>
<RotateCcw size={18} />
Restart
</button>
<button
className="btn btn-secondary"
onClick={handleRegenerate}
disabled={isAnyMutationPending}
>
<RefreshCw size={18} />
Regenerate Config
</button>
</div>
</div>
<div className="status-section">
<h2>Service Information</h2>
<div className="info-grid">
<div className="info-item">
<span className="info-label">Status:</span>
<span className={`info-value ${frpcStatus?.running ? 'text-green-500' : 'text-red-500'}`}>
{frpcStatus?.running ? 'Running' : 'Stopped'}
</span>
</div>
<div className="info-item">
<span className="info-label">Container:</span>
<span className="info-value">frpc</span>
</div>
<div className="info-item">
<span className="info-label">Last Updated:</span>
<span className="info-value">
{new Date().toLocaleString()}
</span>
</div>
</div>
</div>
</div>
<div className="logs-section">
<div className="logs-header">
<h2>
<FileText size={20} />
Service Logs
</h2>
<div className="logs-controls">
<select
value={logsLines}
onChange={(e) => setLogsLines(parseInt(e.target.value))}
className="logs-select"
>
<option value={25}>Last 25 lines</option>
<option value={50}>Last 50 lines</option>
<option value={100}>Last 100 lines</option>
<option value={200}>Last 200 lines</option>
</select>
<button
className="btn btn-sm btn-secondary"
onClick={() => queryClient.invalidateQueries({ queryKey: ['frpc-logs'] })}
disabled={logsLoading}
>
<RefreshCw size={16} />
Refresh
</button>
</div>
</div>
<div className="logs-container">
{logsLoading ? (
<div className="logs-loading">
<RefreshCw className="animate-spin" size={20} />
<span>Loading logs...</span>
</div>
) : (
<pre className="logs-content">
{logs?.logs || 'No logs available'}
</pre>
)}
</div>
</div>
<div className="system-info">
<h2>System Information</h2>
<div className="info-cards">
<div className="info-card">
<div className="info-card-header">
<Server size={24} />
<h3>FRPC Service</h3>
</div>
<div className="info-card-content">
<p>Fast Reverse Proxy Client for tunneling services</p>
<p className="info-detail">
Status: <span className={frpcStatus?.running ? 'text-green-500' : 'text-red-500'}>
{frpcStatus?.running ? 'Active' : 'Inactive'}
</span>
</p>
</div>
</div>
<div className="info-card">
<div className="info-card-header">
<Activity size={24} />
<h3>API Server</h3>
</div>
<div className="info-card-content">
<p>RESTful API for managing tunnel configurations</p>
<p className="info-detail">
Status: <span className="text-green-500">Running</span>
</p>
</div>
</div>
<div className="info-card">
<div className="info-card-header">
<FileText size={24} />
<h3>Configuration</h3>
</div>
<div className="info-card-content">
<p>Tunnel configurations stored in SQLite database</p>
<p className="info-detail">
Auto-generated FRPC config from active tunnels
</p>
</div>
</div>
</div>
</div>
</div>
);
};
export default ServerStatus;

View file

@ -0,0 +1,244 @@
import React, { useState } from 'react';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import { tunnelApi, nodeApi, TunnelConfig } from '../api/client';
import { Plus, Edit2, Trash2, Power, PowerOff, Settings, Send, RefreshCw } from 'lucide-react';
import { toast } from 'react-hot-toast';
import TunnelForm from '../components/TunnelForm';
const TunnelManager: React.FC = () => {
const [showForm, setShowForm] = useState(false);
const [editingTunnel, setEditingTunnel] = useState<TunnelConfig | null>(null);
const queryClient = useQueryClient();
const { data: tunnels, isLoading } = useQuery({
queryKey: ['tunnels'],
queryFn: tunnelApi.getAllTunnels,
refetchInterval: 5000,
});
const { data: tunnelStatuses } = useQuery({
queryKey: ['tunnel-statuses'],
queryFn: tunnelApi.getAllTunnelStatuses,
refetchInterval: 10000,
});
const { data: nodeConnection } = useQuery({
queryKey: ['node-connection'],
queryFn: nodeApi.getConnection,
refetchInterval: 30000,
retry: false,
});
const deleteMutation = useMutation({
mutationFn: tunnelApi.deleteTunnel,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
toast.success('Tunnel deleted successfully');
},
onError: (error) => {
console.error('Delete error:', error);
toast.error('Failed to delete tunnel');
},
});
const updateMutation = useMutation({
mutationFn: ({ id, updates }: { id: string; updates: Partial<TunnelConfig> }) =>
tunnelApi.updateTunnel(id, updates),
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
toast.success('Tunnel updated successfully');
},
onError: (error) => {
console.error('Update error:', error);
toast.error('Failed to update tunnel');
},
});
const pushToNodeMutation = useMutation({
mutationFn: nodeApi.pushAndRestart,
onSuccess: (data) => {
toast.success(`Successfully pushed ${data.tunnelCount} tunnels to node and restarted frpc`);
queryClient.invalidateQueries({ queryKey: ['node-connection'] });
},
onError: (error: any) => {
console.error('Push to node error:', error);
toast.error(error.response?.data?.error || 'Failed to push configuration to node');
},
});
const handleDelete = (id: string) => {
if (window.confirm('Are you sure you want to delete this tunnel?')) {
deleteMutation.mutate(id);
}
};
const handleEdit = (tunnel: TunnelConfig) => {
setEditingTunnel(tunnel);
setShowForm(true);
};
const handleToggleEnabled = (tunnel: TunnelConfig) => {
updateMutation.mutate({
id: tunnel.id!,
updates: { enabled: !tunnel.enabled },
});
};
const handleFormClose = () => {
setShowForm(false);
setEditingTunnel(null);
};
const handleFormSubmit = () => {
handleFormClose();
queryClient.invalidateQueries({ queryKey: ['tunnels'] });
};
const handlePushToNode = () => {
if (window.confirm('Push current tunnel configuration to node and restart frpc?')) {
pushToNodeMutation.mutate();
}
};
if (isLoading) {
return (
<div className="tunnel-manager">
<div className="loading">Loading tunnels...</div>
</div>
);
}
return (
<div className="tunnel-manager">
<div className="tunnel-header">
<h1>Tunnel Manager</h1>
<div className="header-actions">
{nodeConnection && (
<div className="node-status">
<span className={`node-indicator ${nodeConnection.isReachable ? 'online' : 'offline'}`}>
Node: {nodeConnection.isReachable ? 'Online' : 'Offline'}
</span>
<button
className="btn btn-secondary"
onClick={handlePushToNode}
disabled={pushToNodeMutation.isPending || !nodeConnection.isReachable}
title="Push configuration to node and restart frpc"
>
{pushToNodeMutation.isPending ? (
<RefreshCw size={18} className="spinning" />
) : (
<Send size={18} />
)}
Push to Node
</button>
</div>
)}
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
>
<Plus size={18} />
Add Tunnel
</button>
</div>
</div>
<div className="tunnels-grid">
{tunnels?.map(tunnel => {
const status = tunnelStatuses?.find(s => s.id === tunnel.id);
return (
<div key={tunnel.id} className="tunnel-card">
<div className="tunnel-card-header">
<div className="tunnel-title">
<h3>{tunnel.name}</h3>
<div className="tunnel-badges">
<span className={`badge ${tunnel.enabled ? 'badge-success' : 'badge-secondary'}`}>
{tunnel.enabled ? 'Enabled' : 'Disabled'}
</span>
<span className={`badge ${status?.active ? 'badge-active' : 'badge-inactive'}`}>
{status?.active ? 'Active' : 'Inactive'}
</span>
</div>
</div>
<div className="tunnel-actions">
<button
className={`btn btn-sm ${tunnel.enabled ? 'btn-warning' : 'btn-success'}`}
onClick={() => handleToggleEnabled(tunnel)}
disabled={updateMutation.isPending}
>
{tunnel.enabled ? <PowerOff size={16} /> : <Power size={16} />}
</button>
<button
className="btn btn-sm btn-secondary"
onClick={() => handleEdit(tunnel)}
>
<Edit2 size={16} />
</button>
<button
className="btn btn-sm btn-danger"
onClick={() => handleDelete(tunnel.id!)}
disabled={deleteMutation.isPending}
>
<Trash2 size={16} />
</button>
</div>
</div>
<div className="tunnel-info">
<div className="info-row">
<span className="info-label">Protocol:</span>
<span className="info-value">{tunnel.protocol}</span>
</div>
<div className="info-row">
<span className="info-label">Local:</span>
<span className="info-value">{tunnel.localIp}:{tunnel.localPort}</span>
</div>
<div className="info-row">
<span className="info-label">Remote:</span>
<span className="info-value">:{tunnel.remotePort}</span>
</div>
<div className="info-row">
<span className="info-label">Created:</span>
<span className="info-value">
{tunnel.createdAt ? new Date(tunnel.createdAt).toLocaleDateString() : 'N/A'}
</span>
</div>
</div>
{status?.error && (
<div className="tunnel-error">
<span className="error-text">Error: {status.error}</span>
</div>
)}
</div>
);
})}
</div>
{tunnels?.length === 0 && (
<div className="empty-state">
<Settings size={64} className="empty-icon" />
<h3>No tunnels configured</h3>
<p>Get started by adding your first tunnel configuration</p>
<button
className="btn btn-primary"
onClick={() => setShowForm(true)}
>
<Plus size={18} />
Add Your First Tunnel
</button>
</div>
)}
{showForm && (
<TunnelForm
tunnel={editingTunnel}
onClose={handleFormClose}
onSubmit={handleFormSubmit}
/>
)}
</div>
);
};
export default TunnelManager;

View file

@ -0,0 +1,8 @@
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "Bundler",
"jsx": "react-jsx"
}
}

1
app/src/client/vite-env.d.ts vendored Normal file
View file

@ -0,0 +1 @@
/// <reference types="vite/client" />

179
app/src/server/database.ts Normal file
View file

@ -0,0 +1,179 @@
import Database from 'better-sqlite3';
import { TunnelConfig, TunnelConfigSchema, TunnelConfigUpdate } from './types.js';
import { v4 as uuidv4 } from 'uuid';
import path from 'path';
export class TunnelDatabase {
private db: Database.Database;
constructor(dbPath: string = 'data/tunnels.db') {
this.db = new Database(dbPath);
this.initializeDatabase();
}
private initializeDatabase() {
// Create tunnels table
this.db.exec(`
CREATE TABLE IF NOT EXISTS tunnels (
id TEXT PRIMARY KEY,
name TEXT NOT NULL UNIQUE,
protocol TEXT NOT NULL CHECK(protocol IN ('TCP', 'UDP')),
local_ip TEXT NOT NULL,
local_port INTEGER NOT NULL,
remote_port INTEGER NOT NULL,
enabled BOOLEAN DEFAULT 1,
created_at TEXT DEFAULT CURRENT_TIMESTAMP,
updated_at TEXT DEFAULT CURRENT_TIMESTAMP
)
`);
// Create trigger to update updated_at column
this.db.exec(`
CREATE TRIGGER IF NOT EXISTS update_tunnels_updated_at
AFTER UPDATE ON tunnels
BEGIN
UPDATE tunnels SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
END
`);
}
// Get all tunnel configurations
getAllTunnels(): TunnelConfig[] {
const stmt = this.db.prepare(`
SELECT
id,
name,
protocol,
local_ip as localIp,
local_port as localPort,
remote_port as remotePort,
enabled,
created_at as createdAt,
updated_at as updatedAt
FROM tunnels
ORDER BY created_at DESC
`);
return stmt.all() as TunnelConfig[];
}
// Get tunnel by ID
getTunnelById(id: string): TunnelConfig | null {
const stmt = this.db.prepare(`
SELECT
id,
name,
protocol,
local_ip as localIp,
local_port as localPort,
remote_port as remotePort,
enabled,
created_at as createdAt,
updated_at as updatedAt
FROM tunnels
WHERE id = ?
`);
const result = stmt.get(id) as TunnelConfig | undefined;
return result || null;
}
// Create new tunnel configuration
createTunnel(config: Omit<TunnelConfig, 'id' | 'createdAt' | 'updatedAt'>): TunnelConfig {
const id = uuidv4();
const now = new Date().toISOString();
const stmt = this.db.prepare(`
INSERT INTO tunnels (id, name, protocol, local_ip, local_port, remote_port, enabled)
VALUES (?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(id, config.name, config.protocol, config.localIp, config.localPort, config.remotePort, config.enabled);
return {
id,
...config,
createdAt: now,
updatedAt: now
};
}
// Update tunnel configuration
updateTunnel(id: string, updates: Partial<TunnelConfig>): TunnelConfig | null {
const existing = this.getTunnelById(id);
if (!existing) return null;
const fields = [];
const values = [];
if (updates.name !== undefined) {
fields.push('name = ?');
values.push(updates.name);
}
if (updates.protocol !== undefined) {
fields.push('protocol = ?');
values.push(updates.protocol);
}
if (updates.localIp !== undefined) {
fields.push('local_ip = ?');
values.push(updates.localIp);
}
if (updates.localPort !== undefined) {
fields.push('local_port = ?');
values.push(updates.localPort);
}
if (updates.remotePort !== undefined) {
fields.push('remote_port = ?');
values.push(updates.remotePort);
}
if (updates.enabled !== undefined) {
fields.push('enabled = ?');
values.push(updates.enabled);
}
if (fields.length === 0) return existing;
values.push(id);
const stmt = this.db.prepare(`
UPDATE tunnels
SET ${fields.join(', ')}
WHERE id = ?
`);
stmt.run(...values);
return this.getTunnelById(id);
}
// Delete tunnel configuration
deleteTunnel(id: string): boolean {
const stmt = this.db.prepare('DELETE FROM tunnels WHERE id = ?');
const result = stmt.run(id);
return result.changes > 0;
}
// Get enabled tunnels only
getEnabledTunnels(): TunnelConfig[] {
const stmt = this.db.prepare(`
SELECT
id,
name,
protocol,
local_ip as localIp,
local_port as localPort,
remote_port as remotePort,
enabled,
created_at as createdAt,
updated_at as updatedAt
FROM tunnels
WHERE enabled = 1
ORDER BY created_at DESC
`);
return stmt.all() as TunnelConfig[];
}
// Close database connection
close() {
this.db.close();
}
}

View file

@ -0,0 +1,145 @@
import { TunnelConfig, FrpcConfig } from './types.js';
import { exec } from 'child_process';
import { promisify } from 'util';
import fs from 'fs/promises';
import path from 'path';
import { logger } from './logger.js';
const execAsync = promisify(exec);
export class FrpcManager {
private configPath: string;
private containerName: string;
constructor(configPath: string = 'data/frpc.toml', containerName: string = 'frpc') {
this.configPath = configPath;
this.containerName = containerName;
}
// Generate frpc.toml configuration from tunnel configs
async generateConfig(tunnels: TunnelConfig[], serverAddr: string, serverPort: number = 7000, token?: string): Promise<void> {
try {
const config: FrpcConfig = {
serverAddr,
serverPort,
token,
proxies: {}
};
// Add enabled tunnels to config
for (const tunnel of tunnels.filter(t => t.enabled)) {
config.proxies[tunnel.name] = {
type: tunnel.protocol.toLowerCase() as 'tcp' | 'udp',
localIP: tunnel.localIp,
localPort: tunnel.localPort,
remotePort: tunnel.remotePort
};
}
const tomlContent = this.generateTomlContent(config);
// Ensure directory exists
const dir = path.dirname(this.configPath);
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(this.configPath, tomlContent);
logger.info(`Generated frpc configuration with ${Object.keys(config.proxies).length} tunnels`);
} catch (error) {
logger.error('Failed to generate frpc configuration:', error);
throw error;
}
}
private generateTomlContent(config: FrpcConfig): string {
let toml = `[common]
server_addr = "${config.serverAddr}"
server_port = ${config.serverPort}
`;
if (config.token) {
toml += `token = "${config.token}"\n`;
}
toml += '\n';
// Add proxy configurations
for (const [name, proxy] of Object.entries(config.proxies)) {
toml += `[${name}]
type = "${proxy.type}"
local_ip = "${proxy.localIP}"
local_port = ${proxy.localPort}
remote_port = ${proxy.remotePort}
`;
}
return toml;
}
// Check if frpc container is running
async isRunning(): Promise<boolean> {
try {
const { stdout } = await execAsync(`docker ps --filter "name=${this.containerName}" --format "{{.Names}}"`);
return stdout.trim() === this.containerName;
} catch (error) {
logger.error('Failed to check frpc container status:', error);
return false;
}
}
// Start frpc container
async start(): Promise<void> {
try {
await execAsync(`docker start ${this.containerName}`);
logger.info(`Started frpc container: ${this.containerName}`);
} catch (error) {
logger.error('Failed to start frpc container:', error);
throw error;
}
}
// Stop frpc container
async stop(): Promise<void> {
try {
await execAsync(`docker stop ${this.containerName}`);
logger.info(`Stopped frpc container: ${this.containerName}`);
} catch (error) {
logger.error('Failed to stop frpc container:', error);
throw error;
}
}
// Restart frpc container
async restart(): Promise<void> {
try {
await execAsync(`docker restart ${this.containerName}`);
logger.info(`Restarted frpc container: ${this.containerName}`);
} catch (error) {
logger.error('Failed to restart frpc container:', error);
throw error;
}
}
// Get frpc container logs
async getLogs(lines: number = 50): Promise<string> {
try {
const { stdout } = await execAsync(`docker logs --tail ${lines} ${this.containerName}`);
return stdout;
} catch (error) {
logger.error('Failed to get frpc container logs:', error);
throw error;
}
}
// Check tunnel status by attempting to connect
async checkTunnelStatus(tunnel: TunnelConfig): Promise<boolean> {
try {
// This is a basic implementation - you might want to implement actual connectivity checks
// For now, we'll just check if the container is running
return await this.isRunning();
} catch (error) {
logger.error(`Failed to check tunnel status for ${tunnel.name}:`, error);
return false;
}
}
}

42
app/src/server/logger.ts Normal file
View file

@ -0,0 +1,42 @@
import winston from 'winston';
import path from 'path';
// Create logs directory if it doesn't exist
const logDir = 'logs';
export const logger = winston.createLogger({
level: process.env.NODE_ENV === 'production' ? 'info' : 'debug',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json()
),
defaultMeta: { service: 'frpc-manager' },
transports: [
// Write all logs with level 'error' and below to error.log
new winston.transports.File({
filename: path.join(logDir, 'error.log'),
level: 'error',
handleExceptions: true,
handleRejections: true
}),
// Write all logs with level 'info' and below to combined.log
new winston.transports.File({
filename: path.join(logDir, 'combined.log'),
handleExceptions: true,
handleRejections: true
}),
],
});
// If we're not in production, also log to console with simple format
if (process.env.NODE_ENV !== 'production') {
logger.add(new winston.transports.Console({
format: winston.format.combine(
winston.format.colorize(),
winston.format.simple()
)
}));
}
export default logger;

55
app/src/server/main.ts Normal file
View file

@ -0,0 +1,55 @@
import express from "express";
import ViteExpress from "vite-express";
import cors from "cors";
import path from "path";
import { fileURLToPath } from "url";
import { logger } from "./logger.js";
import routes from "./routes.js";
const app = express();
// Get __dirname in ES modules
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
// Middleware
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Logging middleware
app.use((req, res, next) => {
logger.info(`${req.method} ${req.path}`, {
method: req.method,
path: req.path,
userAgent: req.get('User-Agent'),
ip: req.ip
});
next();
});
// API routes
app.use("/api", routes);
// Health check endpoint
app.get("/health", (_, res) => {
res.json({ status: "ok", timestamp: new Date().toISOString() });
});
// Legacy hello endpoint
app.get("/hello", (_, res) => {
res.send("Hello Vite + React + TypeScript!");
});
// Error handling middleware
app.use((error: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
logger.error('Unhandled error:', error);
res.status(500).json({ error: 'Internal server error' });
});
const PORT = parseInt(process.env.PORT || '3000');
ViteExpress.listen(app, PORT, () => {
logger.info(`Server is listening on port ${PORT}`);
console.log(`Server is listening on port ${PORT}...`);
});

View file

@ -0,0 +1,114 @@
import axios, { AxiosResponse } from 'axios';
import { logger } from './logger.js';
export interface NodeStatus {
status: string;
timestamp: string;
uptime?: number;
memory?: {
used: number;
total: number;
};
cpu?: {
usage: number;
};
}
export interface NodeConfig {
url: string;
token: string;
timeout?: number;
}
export class NodeClient {
private config: NodeConfig;
private lastConnectionTime: Date | null = null;
private isOnline: boolean = false;
constructor(config: NodeConfig) {
this.config = {
...config,
timeout: config.timeout || 5000,
};
}
private async makeRequest<T>(
method: 'GET' | 'POST' | 'PUT' | 'DELETE',
endpoint: string,
data?: any
): Promise<T> {
try {
const response: AxiosResponse<T> = await axios({
method,
url: `${this.config.url}${endpoint}`,
headers: {
'Authorization': `Bearer ${this.config.token}`,
'Content-Type': 'application/json',
},
data,
timeout: this.config.timeout,
});
this.lastConnectionTime = new Date();
this.isOnline = true;
return response.data;
} catch (error) {
this.isOnline = false;
logger.error(`Node request failed: ${method} ${endpoint}`, error);
throw error;
}
}
// Get node status
async getStatus(): Promise<NodeStatus> {
return this.makeRequest<NodeStatus>('GET', '/api/status');
}
// Send updated frpc.toml config to the node
async updateConfig(config: string): Promise<{ success: boolean; message: string }> {
return this.makeRequest<{ success: boolean; message: string }>('POST', '/api/frpc/update-config', {
config,
});
}
// Restart the FRP client on the node
async restartFrpc(): Promise<{ success: boolean; message: string }> {
return this.makeRequest<{ success: boolean; message: string }>('POST', '/api/frpc/restart');
}
// Check if node is reachable
async isReachable(): Promise<boolean> {
try {
await this.makeRequest<any>('GET', '/health');
return true;
} catch (error) {
return false;
}
}
// Get connection info
getConnectionInfo() {
return {
url: this.config.url,
isOnline: this.isOnline,
lastConnectionTime: this.lastConnectionTime,
};
}
}
// Factory function to create node client with environment variables
export function createNodeClient(): NodeClient {
const nodeUrl = process.env.NODE_URL;
const nodeToken = process.env.NODE_TOKEN;
if (!nodeUrl || !nodeToken) {
throw new Error('NODE_URL and NODE_TOKEN environment variables are required');
}
return new NodeClient({
url: nodeUrl,
token: nodeToken,
timeout: parseInt(process.env.NODE_TIMEOUT || '5000'),
});
}

376
app/src/server/routes.ts Normal file
View file

@ -0,0 +1,376 @@
import express, { Request, Response, NextFunction } from 'express';
import { TunnelDatabase } from './database.js';
import { FrpcManager } from './frpc-manager.js';
import { createNodeClient } from './node-client.js';
import { TunnelConfigSchema, TunnelConfigUpdateSchema, TunnelConfig, TunnelStatus } from './types.js';
import { logger } from './logger.js';
const router = express.Router();
// Initialize services
const db = new TunnelDatabase();
const frpcManager = new FrpcManager();
// Initialize node client if configured
let nodeClient: ReturnType<typeof createNodeClient> | null = null;
try {
nodeClient = createNodeClient();
logger.info('Node client initialized successfully');
} catch (error) {
logger.warn('Node client not configured:', error instanceof Error ? error.message : 'Unknown error');
}
// Async handler wrapper
const asyncHandler = (fn: (req: Request, res: Response, next: NextFunction) => Promise<any>) => {
return (req: Request, res: Response, next: NextFunction) => {
Promise.resolve(fn(req, res, next)).catch(next);
};
};
// Get all tunnels
router.get('/tunnels', asyncHandler(async (req: Request, res: Response) => {
const tunnels = db.getAllTunnels();
res.json(tunnels);
}));
// Get tunnel by ID
router.get('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
const tunnel = db.getTunnelById(req.params.id);
if (!tunnel) {
return res.status(404).json({ error: 'Tunnel not found' });
}
res.json(tunnel);
}));
// Create new tunnel
router.post('/tunnels', asyncHandler(async (req: Request, res: Response) => {
const validation = TunnelConfigSchema.safeParse(req.body);
if (!validation.success) {
return res.status(400).json({
error: 'Invalid tunnel configuration',
details: validation.error.errors
});
}
const tunnel = db.createTunnel(validation.data);
// Regenerate frpc config if tunnel is enabled
if (tunnel.enabled) {
await regenerateFrpcConfig();
}
res.status(201).json(tunnel);
}));
// Update tunnel
router.put('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
const validation = TunnelConfigUpdateSchema.safeParse({
...req.body,
id: req.params.id
});
if (!validation.success) {
return res.status(400).json({
error: 'Invalid tunnel configuration',
details: validation.error.errors
});
}
const tunnel = db.updateTunnel(req.params.id, validation.data);
if (!tunnel) {
return res.status(404).json({ error: 'Tunnel not found' });
}
// Regenerate frpc config
await regenerateFrpcConfig();
res.json(tunnel);
}));
// Delete tunnel
router.delete('/tunnels/:id', asyncHandler(async (req: Request, res: Response) => {
const deleted = db.deleteTunnel(req.params.id);
if (!deleted) {
return res.status(404).json({ error: 'Tunnel not found' });
}
// Regenerate frpc config
await regenerateFrpcConfig();
res.status(204).send();
}));
// Get tunnel status
router.get('/tunnels/:id/status', asyncHandler(async (req: Request, res: Response) => {
const tunnel = db.getTunnelById(req.params.id);
if (!tunnel) {
return res.status(404).json({ error: 'Tunnel not found' });
}
const active = await frpcManager.checkTunnelStatus(tunnel);
const status: TunnelStatus = {
id: tunnel.id!,
name: tunnel.name,
active,
lastChecked: new Date().toISOString()
};
res.json(status);
}));
// Get all tunnel statuses
router.get('/tunnels-status', asyncHandler(async (req: Request, res: Response) => {
const tunnels = db.getAllTunnels();
const statuses: TunnelStatus[] = [];
for (const tunnel of tunnels) {
try {
const active = await frpcManager.checkTunnelStatus(tunnel);
statuses.push({
id: tunnel.id!,
name: tunnel.name,
active,
lastChecked: new Date().toISOString()
});
} catch (error) {
statuses.push({
id: tunnel.id!,
name: tunnel.name,
active: false,
lastChecked: new Date().toISOString(),
error: error instanceof Error ? error.message : 'Unknown error'
});
}
}
res.json(statuses);
}));
// Control frpc service
router.post('/frpc/:action', asyncHandler(async (req: Request, res: Response) => {
const { action } = req.params;
switch (action) {
case 'start':
await frpcManager.start();
break;
case 'stop':
await frpcManager.stop();
break;
case 'restart':
await frpcManager.restart();
break;
case 'regenerate':
await regenerateFrpcConfig();
break;
default:
return res.status(400).json({ error: 'Invalid action' });
}
res.json({ message: `frpc ${action} completed successfully` });
}));
// Get frpc status
router.get('/frpc/status', asyncHandler(async (req: Request, res: Response) => {
const running = await frpcManager.isRunning();
res.json({ running });
}));
// Get frpc logs
router.get('/frpc/logs', asyncHandler(async (req: Request, res: Response) => {
const lines = parseInt(req.query.lines as string) || 50;
const logs = await frpcManager.getLogs(lines);
res.json({ logs });
}));
// Helper function to regenerate frpc config and restart if needed
async function regenerateFrpcConfig() {
const enabledTunnels = db.getEnabledTunnels();
// Get server configuration from environment variables
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
const token = process.env.FRPC_TOKEN;
await frpcManager.generateConfig(enabledTunnels, serverAddr, serverPort, token);
// Restart frpc if it's running
if (await frpcManager.isRunning()) {
await frpcManager.restart();
}
}
// Node management endpoints
router.get('/node/status', asyncHandler(async (req: Request, res: Response) => {
if (!nodeClient) {
return res.status(503).json({ error: 'Node client not configured' });
}
try {
const status = await nodeClient.getStatus();
const connectionInfo = nodeClient.getConnectionInfo();
res.json({
...status,
connection: connectionInfo
});
} catch (error) {
logger.error('Failed to get node status:', error);
res.status(500).json({
error: 'Failed to connect to node',
connection: nodeClient.getConnectionInfo()
});
}
}));
router.get('/node/connection', asyncHandler(async (req: Request, res: Response) => {
if (!nodeClient) {
return res.status(503).json({ error: 'Node client not configured' });
}
const connectionInfo = nodeClient.getConnectionInfo();
const isReachable = await nodeClient.isReachable();
res.json({
...connectionInfo,
isReachable
});
}));
router.post('/node/push-config', asyncHandler(async (req: Request, res: Response) => {
if (!nodeClient) {
return res.status(503).json({ error: 'Node client not configured' });
}
try {
// Regenerate frpc config
await regenerateFrpcConfig();
// Read the generated config
const enabledTunnels = db.getEnabledTunnels();
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
const token = process.env.FRPC_TOKEN;
// Generate config content
let tomlContent = `[common]
server_addr = "${serverAddr}"
server_port = ${serverPort}
`;
if (token) {
tomlContent += `token = "${token}"\n`;
}
tomlContent += '\n';
// Add proxy configurations
for (const tunnel of enabledTunnels) {
tomlContent += `[${tunnel.name}]
type = "${tunnel.protocol.toLowerCase()}"
local_ip = "${tunnel.localIp}"
local_port = ${tunnel.localPort}
remote_port = ${tunnel.remotePort}
`;
}
// Send config to node
const result = await nodeClient.updateConfig(tomlContent);
logger.info('Configuration pushed to node successfully');
res.json({
message: 'Configuration pushed to node successfully',
tunnelCount: enabledTunnels.length,
nodeResponse: result
});
} catch (error) {
logger.error('Failed to push config to node:', error);
res.status(500).json({
error: 'Failed to push configuration to node',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}));
router.post('/node/restart-frpc', asyncHandler(async (req: Request, res: Response) => {
if (!nodeClient) {
return res.status(503).json({ error: 'Node client not configured' });
}
try {
const result = await nodeClient.restartFrpc();
logger.info('frpc restarted on node successfully');
res.json({
message: 'frpc restarted on node successfully',
nodeResponse: result
});
} catch (error) {
logger.error('Failed to restart frpc on node:', error);
res.status(500).json({
error: 'Failed to restart frpc on node',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}));
router.post('/node/push-and-restart', asyncHandler(async (req: Request, res: Response) => {
if (!nodeClient) {
return res.status(503).json({ error: 'Node client not configured' });
}
try {
// First push the config
await regenerateFrpcConfig();
// Read the generated config
const enabledTunnels = db.getEnabledTunnels();
const serverAddr = process.env.FRPC_SERVER_ADDR || 'your-vps-ip';
const serverPort = parseInt(process.env.FRPC_SERVER_PORT || '7000');
const token = process.env.FRPC_TOKEN;
// Generate config content
let tomlContent = `[common]
server_addr = "${serverAddr}"
server_port = ${serverPort}
`;
if (token) {
tomlContent += `token = "${token}"\n`;
}
tomlContent += '\n';
// Add proxy configurations
for (const tunnel of enabledTunnels) {
tomlContent += `[${tunnel.name}]
type = "${tunnel.protocol.toLowerCase()}"
local_ip = "${tunnel.localIp}"
local_port = ${tunnel.localPort}
remote_port = ${tunnel.remotePort}
`;
}
// Send config to node and restart in one call
const result = await nodeClient.updateConfig(tomlContent);
const restartResult = await nodeClient.restartFrpc();
logger.info('Configuration pushed and frpc restarted on node successfully');
res.json({
message: 'Configuration pushed and frpc restarted on node successfully',
tunnelCount: enabledTunnels.length,
configResponse: result,
restartResponse: restartResult
});
} catch (error) {
logger.error('Failed to push config and restart frpc on node:', error);
res.status(500).json({
error: 'Failed to push configuration and restart frpc on node',
details: error instanceof Error ? error.message : 'Unknown error'
});
}
}));
export default router;

43
app/src/server/types.ts Normal file
View file

@ -0,0 +1,43 @@
import { z } from 'zod';
// Zod schemas for validation
export const TunnelConfigSchema = z.object({
id: z.string().optional(),
name: z.string().min(1, 'Name is required'),
protocol: z.enum(['TCP', 'UDP']),
localIp: z.string().min(1, 'Local IP is required'),
localPort: z.number().int().min(1).max(65535),
remotePort: z.number().int().min(1).max(65535),
enabled: z.boolean().default(true),
createdAt: z.string().optional(),
updatedAt: z.string().optional(),
});
export const TunnelConfigUpdateSchema = TunnelConfigSchema.partial().extend({
id: z.string(),
});
export type TunnelConfig = z.infer<typeof TunnelConfigSchema>;
export type TunnelConfigUpdate = z.infer<typeof TunnelConfigUpdateSchema>;
export interface TunnelStatus {
id: string;
name: string;
active: boolean;
lastChecked: string;
error?: string;
}
export interface FrpcConfig {
serverAddr: string;
serverPort: number;
token?: string;
proxies: {
[key: string]: {
type: 'tcp' | 'udp';
localIP: string;
localPort: number;
remotePort: number;
};
};
}

19
app/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"target": "ESNext",
"useDefineForClassFields": true,
"lib": ["DOM", "DOM.Iterable", "ESNext"],
"allowJs": false,
"skipLibCheck": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"strict": true,
"forceConsistentCasingInFileNames": true,
"module": "NodeNext",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true
},
"include": ["src"]
}

7
app/vite.config.ts Normal file
View file

@ -0,0 +1,7 @@
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
});