From 4169337dd0eed013a1c0f836028155cda15b88cc Mon Sep 17 00:00:00 2001 From: hunternick87 <47934810+hunternick87@users.noreply.github.com> Date: Thu, 3 Jul 2025 15:50:13 -0400 Subject: [PATCH] First --- .forgejo/workflows/build-all.yml | 145 ++++ .forgejo/workflows/build-app.yml | 69 ++ .forgejo/workflows/build-node.yml | 69 ++ .forgejo/workflows/deploy.yml | 157 ++++ .forgejo/workflows/test.yml | 104 +++ CI_CD_DOCUMENTATION.md | 297 ++++++++ NODE_INTEGRATION.md | 229 ++++++ app/.env | 6 + app/.env.example | 22 + app/.gitignore | 24 + app/DEPLOYMENT.md | 336 +++++++++ app/Dockerfile | 29 + app/README.md | 270 +++++++ app/bun.lock | 862 +++++++++++++++++++++ app/data/frpc.toml | 15 + app/data/tunnels.db | Bin 0 -> 16384 bytes app/docker-compose.override.yml | 28 + app/docker-compose.yml | 39 + app/health-check.ps1 | 69 ++ app/health-check.sh | 58 ++ app/index.html | 13 + app/package.json | 44 ++ app/public/vite.svg | 1 + app/src/client/App.css | 922 +++++++++++++++++++++++ app/src/client/App.tsx | 51 ++ app/src/client/api/client.ts | 199 +++++ app/src/client/assets/react.svg | 1 + app/src/client/components/Navbar.tsx | 43 ++ app/src/client/components/TunnelForm.tsx | 232 ++++++ app/src/client/index.css | 69 ++ app/src/client/main.tsx | 12 + app/src/client/pages/Dashboard.tsx | 213 ++++++ app/src/client/pages/ServerStatus.tsx | 253 +++++++ app/src/client/pages/TunnelManager.tsx | 244 ++++++ app/src/client/tsconfig.json | 8 + app/src/client/vite-env.d.ts | 1 + app/src/server/database.ts | 179 +++++ app/src/server/frpc-manager.ts | 145 ++++ app/src/server/logger.ts | 42 ++ app/src/server/main.ts | 55 ++ app/src/server/node-client.ts | 114 +++ app/src/server/routes.ts | 376 +++++++++ app/src/server/types.ts | 43 ++ app/tsconfig.json | 19 + app/vite.config.ts | 7 + node/.dockerignore | 15 + node/.env.example | 26 + node/.gitignore | 34 + node/DEPLOYMENT.md | 380 ++++++++++ node/Dockerfile | 56 ++ node/README.md | 285 +++++++ node/bun.lock | 430 +++++++++++ node/docker-compose.dev.yml | 49 ++ node/docker-compose.yml | 153 ++++ node/health-check.ps1 | 110 +++ node/health-check.sh | 98 +++ node/home-server-agent.service | 23 + node/install-windows-service.ps1 | 54 ++ node/package.json | 34 + node/src/index.ts | 83 ++ node/src/middleware/auth.ts | 25 + node/src/routes/frpc.ts | 158 ++++ node/src/routes/gameServer.ts | 138 ++++ node/src/routes/status.ts | 78 ++ node/src/services/dockerManager.ts | 247 ++++++ node/src/utils/logger.ts | 26 + node/test-api.js | 67 ++ node/tsconfig.json | 43 ++ 68 files changed, 8726 insertions(+) create mode 100644 .forgejo/workflows/build-all.yml create mode 100644 .forgejo/workflows/build-app.yml create mode 100644 .forgejo/workflows/build-node.yml create mode 100644 .forgejo/workflows/deploy.yml create mode 100644 .forgejo/workflows/test.yml create mode 100644 CI_CD_DOCUMENTATION.md create mode 100644 NODE_INTEGRATION.md create mode 100644 app/.env create mode 100644 app/.env.example create mode 100644 app/.gitignore create mode 100644 app/DEPLOYMENT.md create mode 100644 app/Dockerfile create mode 100644 app/README.md create mode 100644 app/bun.lock create mode 100644 app/data/frpc.toml create mode 100644 app/data/tunnels.db create mode 100644 app/docker-compose.override.yml create mode 100644 app/docker-compose.yml create mode 100644 app/health-check.ps1 create mode 100644 app/health-check.sh create mode 100644 app/index.html create mode 100644 app/package.json create mode 100644 app/public/vite.svg create mode 100644 app/src/client/App.css create mode 100644 app/src/client/App.tsx create mode 100644 app/src/client/api/client.ts create mode 100644 app/src/client/assets/react.svg create mode 100644 app/src/client/components/Navbar.tsx create mode 100644 app/src/client/components/TunnelForm.tsx create mode 100644 app/src/client/index.css create mode 100644 app/src/client/main.tsx create mode 100644 app/src/client/pages/Dashboard.tsx create mode 100644 app/src/client/pages/ServerStatus.tsx create mode 100644 app/src/client/pages/TunnelManager.tsx create mode 100644 app/src/client/tsconfig.json create mode 100644 app/src/client/vite-env.d.ts create mode 100644 app/src/server/database.ts create mode 100644 app/src/server/frpc-manager.ts create mode 100644 app/src/server/logger.ts create mode 100644 app/src/server/main.ts create mode 100644 app/src/server/node-client.ts create mode 100644 app/src/server/routes.ts create mode 100644 app/src/server/types.ts create mode 100644 app/tsconfig.json create mode 100644 app/vite.config.ts create mode 100644 node/.dockerignore create mode 100644 node/.env.example create mode 100644 node/.gitignore create mode 100644 node/DEPLOYMENT.md create mode 100644 node/Dockerfile create mode 100644 node/README.md create mode 100644 node/bun.lock create mode 100644 node/docker-compose.dev.yml create mode 100644 node/docker-compose.yml create mode 100644 node/health-check.ps1 create mode 100644 node/health-check.sh create mode 100644 node/home-server-agent.service create mode 100644 node/install-windows-service.ps1 create mode 100644 node/package.json create mode 100644 node/src/index.ts create mode 100644 node/src/middleware/auth.ts create mode 100644 node/src/routes/frpc.ts create mode 100644 node/src/routes/gameServer.ts create mode 100644 node/src/routes/status.ts create mode 100644 node/src/services/dockerManager.ts create mode 100644 node/src/utils/logger.ts create mode 100644 node/test-api.js create mode 100644 node/tsconfig.json diff --git a/.forgejo/workflows/build-all.yml b/.forgejo/workflows/build-all.yml new file mode 100644 index 0000000..3924987 --- /dev/null +++ b/.forgejo/workflows/build-all.yml @@ -0,0 +1,145 @@ +name: Build All Docker Images + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + release: + types: [ published ] + workflow_dispatch: + +env: + REGISTRY: ghcr.io + +jobs: + changes: + runs-on: ubuntu-latest + outputs: + app: ${{ steps.changes.outputs.app }} + node: ${{ steps.changes.outputs.node }} + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Check for changes + uses: dorny/paths-filter@v2 + id: changes + with: + filters: | + app: + - 'app/**' + - '.forgejo/workflows/build-all.yml' + node: + - 'node/**' + - '.forgejo/workflows/build-all.yml' + + build-app: + needs: changes + if: ${{ needs.changes.outputs.app == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for App + id: meta-app + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ github.repository }}/frp-manager-app + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push App Docker image + uses: docker/build-push-action@v5 + with: + context: ./app + file: ./app/Dockerfile + push: true + tags: ${{ steps.meta-app.outputs.tags }} + labels: ${{ steps.meta-app.outputs.labels }} + cache-from: type=gha,scope=app + cache-to: type=gha,mode=max,scope=app + platforms: linux/amd64,linux/arm64 + + build-node: + needs: changes + if: ${{ needs.changes.outputs.node == 'true' || github.event_name == 'workflow_dispatch' || github.event_name == 'release' }} + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata for Node + id: meta-node + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ github.repository }}/home-server-agent + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Node Docker image + uses: docker/build-push-action@v5 + with: + context: ./node + file: ./node/Dockerfile + push: true + tags: ${{ steps.meta-node.outputs.tags }} + labels: ${{ steps.meta-node.outputs.labels }} + cache-from: type=gha,scope=node + cache-to: type=gha,mode=max,scope=node + platforms: linux/amd64,linux/arm64 + + summary: + needs: [build-app, build-node] + if: always() + runs-on: ubuntu-latest + steps: + - name: Summary + run: | + echo "## Build Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### App Build: ${{ needs.build-app.result }}" >> $GITHUB_STEP_SUMMARY + echo "### Node Build: ${{ needs.build-node.result }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + if [ "${{ github.event_name }}" == "release" ]; then + echo "### Release Images:" >> $GITHUB_STEP_SUMMARY + echo "- App: \`${{ env.REGISTRY }}/${{ github.repository }}/frp-manager-app:${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- Node: \`${{ env.REGISTRY }}/${{ github.repository }}/home-server-agent:${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + fi diff --git a/.forgejo/workflows/build-app.yml b/.forgejo/workflows/build-app.yml new file mode 100644 index 0000000..24284e6 --- /dev/null +++ b/.forgejo/workflows/build-app.yml @@ -0,0 +1,69 @@ +name: Build and Push App Docker Image + +on: + push: + branches: [ main, develop ] + paths: + - 'app/**' + - '.forgejo/workflows/build-app.yml' + pull_request: + branches: [ main ] + paths: + - 'app/**' + release: + types: [ published ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/frp-manager-app + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./app + file: ./app/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + + - name: Update Docker Compose with new image + if: github.event_name == 'release' + run: | + echo "Built and pushed image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}" + echo "Update your docker-compose.yml to use: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}" diff --git a/.forgejo/workflows/build-node.yml b/.forgejo/workflows/build-node.yml new file mode 100644 index 0000000..c1ca9a2 --- /dev/null +++ b/.forgejo/workflows/build-node.yml @@ -0,0 +1,69 @@ +name: Build and Push Node Docker Image + +on: + push: + branches: [ main, develop ] + paths: + - 'node/**' + - '.forgejo/workflows/build-node.yml' + pull_request: + branches: [ main ] + paths: + - 'node/**' + release: + types: [ published ] + +env: + REGISTRY: ghcr.io + IMAGE_NAME: ${{ github.repository }}/home-server-agent + +jobs: + build: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ github.actor }} + password: ${{ secrets.GITHUB_TOKEN }} + + - name: Extract metadata + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=pr + type=semver,pattern={{version}} + type=semver,pattern={{major}}.{{minor}} + type=raw,value=latest,enable={{is_default_branch}} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: ./node + file: ./node/Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64,linux/arm64 + + - name: Update Docker Compose with new image + if: github.event_name == 'release' + run: | + echo "Built and pushed image: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}" + echo "Update your docker-compose.yml to use: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}:${{ github.ref_name }}" diff --git a/.forgejo/workflows/deploy.yml b/.forgejo/workflows/deploy.yml new file mode 100644 index 0000000..6b2cbdf --- /dev/null +++ b/.forgejo/workflows/deploy.yml @@ -0,0 +1,157 @@ +name: Deploy to Production + +on: + release: + types: [ published ] + workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy to' + required: true + default: 'staging' + type: choice + options: + - staging + - production + +env: + REGISTRY: ghcr.io + +jobs: + deploy: + runs-on: ubuntu-latest + environment: ${{ github.event.inputs.environment || 'production' }} + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Set environment variables + run: | + if [ "${{ github.event_name }}" == "release" ]; then + echo "DEPLOY_ENV=production" >> $GITHUB_ENV + echo "IMAGE_TAG=${{ github.ref_name }}" >> $GITHUB_ENV + else + echo "DEPLOY_ENV=${{ github.event.inputs.environment }}" >> $GITHUB_ENV + echo "IMAGE_TAG=latest" >> $GITHUB_ENV + fi + + - name: Create deployment package + run: | + mkdir -p deployment + + # Copy docker-compose files + cp app/docker-compose.yml deployment/docker-compose-app.yml + cp node/docker-compose.yml deployment/docker-compose-node.yml + + # Copy environment templates + cp app/.env.example deployment/.env.app.example + cp node/.env.example deployment/.env.node.example + + # Create deployment script + cat > deployment/deploy.sh << 'EOF' + #!/bin/bash + set -e + + REGISTRY="${{ env.REGISTRY }}" + REPO="${{ github.repository }}" + TAG="${{ env.IMAGE_TAG }}" + + echo "Deploying FRP Manager to ${{ env.DEPLOY_ENV }}" + echo "Using images: $REGISTRY/$REPO/frp-manager-app:$TAG and $REGISTRY/$REPO/home-server-agent:$TAG" + + # Pull latest images + docker pull $REGISTRY/$REPO/frp-manager-app:$TAG + docker pull $REGISTRY/$REPO/home-server-agent:$TAG + + # Update docker-compose files with new image references + sed -i "s|build: \.|image: $REGISTRY/$REPO/frp-manager-app:$TAG|g" docker-compose-app.yml + sed -i "s|build: \.|image: $REGISTRY/$REPO/home-server-agent:$TAG|g" docker-compose-node.yml + + echo "Deployment package ready!" + echo "1. Configure .env files based on .env.*.example" + echo "2. Run: docker-compose -f docker-compose-app.yml up -d" + echo "3. Run: docker-compose -f docker-compose-node.yml up -d (on home server)" + EOF + + chmod +x deployment/deploy.sh + + - name: Create deployment documentation + run: | + cat > deployment/README.md << 'EOF' + # FRP Manager Deployment Package + + This package contains everything needed to deploy the FRP Manager application. + + ## Files + + - `docker-compose-app.yml` - App deployment configuration + - `docker-compose-node.yml` - Node deployment configuration + - `.env.app.example` - App environment template + - `.env.node.example` - Node environment template + - `deploy.sh` - Deployment script + + ## Quick Start + + ### 1. App Server (VPS) + ```bash + # Configure environment + cp .env.app.example .env + # Edit .env with your configuration + + # Deploy + docker-compose -f docker-compose-app.yml up -d + ``` + + ### 2. Home Server + ```bash + # Configure environment + cp .env.node.example .env + # Edit .env with your configuration + + # Deploy + docker-compose -f docker-compose-node.yml up -d + ``` + + ## Environment Variables + + See the `.env.*.example` files for required configuration. + + **Important**: Set matching tokens for `NODE_TOKEN` (app) and `API_TOKEN` (node). + + ## Verification + + 1. Check app: `http://your-vps:3000` + 2. Check node: `http://your-home-server:3001/health` + 3. Test integration via the "Push to Node" button in the web interface + + ## Version + + - App Image: `${{ env.REGISTRY }}/${{ github.repository }}/frp-manager-app:${{ env.IMAGE_TAG }}` + - Node Image: `${{ env.REGISTRY }}/${{ github.repository }}/home-server-agent:${{ env.IMAGE_TAG }}` + EOF + + - name: Package deployment artifacts + run: | + tar -czf frp-manager-deployment-${{ env.IMAGE_TAG }}.tar.gz -C deployment . + + - name: Upload deployment package + uses: actions/upload-artifact@v4 + with: + name: frp-manager-deployment-${{ env.DEPLOY_ENV }}-${{ env.IMAGE_TAG }} + path: frp-manager-deployment-${{ env.IMAGE_TAG }}.tar.gz + retention-days: 90 + + - name: Create deployment summary + run: | + echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Environment:** ${{ env.DEPLOY_ENV }}" >> $GITHUB_STEP_SUMMARY + echo "**Version:** ${{ env.IMAGE_TAG }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Images Built:" >> $GITHUB_STEP_SUMMARY + echo "- App: \`${{ env.REGISTRY }}/${{ github.repository }}/frp-manager-app:${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "- Node: \`${{ env.REGISTRY }}/${{ github.repository }}/home-server-agent:${{ env.IMAGE_TAG }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Deployment Package:" >> $GITHUB_STEP_SUMMARY + echo "Download the deployment artifact and follow the README.md instructions." >> $GITHUB_STEP_SUMMARY diff --git a/.forgejo/workflows/test.yml b/.forgejo/workflows/test.yml new file mode 100644 index 0000000..0661409 --- /dev/null +++ b/.forgejo/workflows/test.yml @@ -0,0 +1,104 @@ +name: Test and Lint + +on: + push: + branches: [ main, develop ] + pull_request: + branches: [ main ] + +jobs: + test-app: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./app + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: './app/package-lock.json' + + - name: Install dependencies + run: npm ci + + - name: Type check + run: npx tsc --noEmit + + - name: Build application + run: npm run build + + - name: Run tests (if available) + run: npm test --if-present + continue-on-error: true + + test-node: + runs-on: ubuntu-latest + defaults: + run: + working-directory: ./node + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Setup Bun + uses: oven-sh/setup-bun@v1 + with: + bun-version: latest + + - name: Install dependencies + run: bun install + + - name: Type check + run: bunx tsc --noEmit + + - name: Build application + run: bun run build + + - name: Run tests (if available) + run: bun test --if-present + continue-on-error: true + + lint-dockerfiles: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Lint App Dockerfile + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ./app/Dockerfile + failure-threshold: warning + + - name: Lint Node Dockerfile + uses: hadolint/hadolint-action@v3.1.0 + with: + dockerfile: ./node/Dockerfile + failure-threshold: warning + + security-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + scan-type: 'fs' + scan-ref: '.' + format: 'sarif' + output: 'trivy-results.sarif' + + - name: Upload Trivy scan results + uses: github/codeql-action/upload-sarif@v2 + if: always() + with: + sarif_file: 'trivy-results.sarif' diff --git a/CI_CD_DOCUMENTATION.md b/CI_CD_DOCUMENTATION.md new file mode 100644 index 0000000..6d0ec72 --- /dev/null +++ b/CI_CD_DOCUMENTATION.md @@ -0,0 +1,297 @@ +# Forgejo Actions CI/CD Documentation + +This document describes the Forgejo Actions workflows for building, testing, and deploying the FRP Manager application. + +## Workflow Overview + +The CI/CD pipeline consists of several automated workflows: + +### 1. **Test and Lint** (`.forgejo/workflows/test.yml`) +- **Triggers**: Push to `main`/`develop`, Pull Requests +- **Purpose**: Code quality, type checking, and security scanning +- **Jobs**: + - `test-app`: TypeScript compilation and build testing for the app + - `test-node`: TypeScript compilation and build testing for the node + - `lint-dockerfiles`: Dockerfile linting with hadolint + - `security-scan`: Vulnerability scanning with Trivy + +### 2. **Build App** (`.forgejo/workflows/build-app.yml`) +- **Triggers**: Changes to `app/` directory, releases +- **Purpose**: Build and push the FRP Manager app Docker image +- **Features**: Multi-platform builds (amd64, arm64), caching + +### 3. **Build Node** (`.forgejo/workflows/build-node.yml`) +- **Triggers**: Changes to `node/` directory, releases +- **Purpose**: Build and push the Home Server Agent Docker image +- **Features**: Multi-platform builds (amd64, arm64), caching + +### 4. **Build All** (`.forgejo/workflows/build-all.yml`) +- **Triggers**: Push to branches, PRs, releases, manual dispatch +- **Purpose**: Intelligent building of both images with change detection +- **Features**: + - Path-based change detection + - Parallel builds when both projects change + - Build summary generation + +### 5. **Deploy** (`.forgejo/workflows/deploy.yml`) +- **Triggers**: Releases, manual dispatch +- **Purpose**: Create deployment packages for production +- **Features**: + - Environment-specific deployments + - Automated deployment package creation + - Documentation generation + +## Container Registry + +Images are published to GitHub Container Registry (GHCR): +- **App**: `ghcr.io/YOUR_USERNAME/YOUR_REPO/frp-manager-app` +- **Node**: `ghcr.io/YOUR_USERNAME/YOUR_REPO/home-server-agent` + +### Image Tags + +- `latest` - Latest build from main branch +- `main` - Latest main branch build +- `develop` - Latest develop branch build +- `v1.0.0` - Specific version tags (on releases) +- `1.0` - Major.minor tags (on releases) + +## Setup Requirements + +### 1. Repository Secrets + +The workflows require these secrets to be configured in your Forgejo repository: + +```bash +# Container registry access (automatically provided by Forgejo/GitHub) +GITHUB_TOKEN # Automatic, no setup needed +``` + +### 2. Repository Settings + +1. **Enable Actions**: Ensure Forgejo Actions are enabled in repository settings +2. **Container Registry**: Enable package publishing permissions +3. **Branch Protection**: Consider protecting `main` branch with required status checks + +### 3. Runner Requirements + +Ensure your Forgejo instance has runners configured with: +- Docker support +- Multi-platform build capabilities (for arm64 builds) +- Sufficient storage for image caching + +## Workflow Triggers + +### Automatic Triggers + +```yaml +# Push to main/develop branches +push: + branches: [ main, develop ] + +# Pull requests to main +pull_request: + branches: [ main ] + +# Release creation +release: + types: [ published ] + +# Path-based triggers (Build All workflow) +paths: + - 'app/**' + - 'node/**' +``` + +### Manual Triggers + +```bash +# Manual workflow dispatch (via UI or API) +workflow_dispatch: + inputs: + environment: + description: 'Environment to deploy to' + required: true + default: 'staging' + type: choice + options: [ staging, production ] +``` + +## Build Process + +### 1. **Code Quality Checks** +```bash +# TypeScript compilation +npx tsc --noEmit # App +bunx tsc --noEmit # Node + +# Application builds +npm run build # App +bun run build # Node + +# Dockerfile linting +hadolint Dockerfile + +# Security scanning +trivy fs . +``` + +### 2. **Docker Image Building** +```bash +# Multi-platform builds +docker buildx build \ + --platform linux/amd64,linux/arm64 \ + --push \ + --tag ghcr.io/user/repo/app:latest \ + ./app + +# With caching +--cache-from type=gha \ +--cache-to type=gha,mode=max +``` + +### 3. **Image Publishing** +- Images are automatically pushed to GHCR +- Tags are generated based on branch/release +- Metadata labels are added for tracking + +## Deployment Process + +### 1. **Automated Deployment Package** +When a release is created or deployment is manually triggered: + +1. **Image Building**: Latest images are built and pushed +2. **Package Creation**: Deployment artifacts are created +3. **Documentation**: Deployment instructions are generated +4. **Artifact Upload**: Package is uploaded for download + +### 2. **Deployment Package Contents** +``` +frp-manager-deployment-v1.0.0.tar.gz +├── docker-compose-app.yml # App deployment config +├── docker-compose-node.yml # Node deployment config +├── .env.app.example # App environment template +├── .env.node.example # Node environment template +├── deploy.sh # Deployment script +└── README.md # Deployment instructions +``` + +### 3. **Manual Deployment Steps** + +1. **Download Package**: Get the deployment artifact from the workflow run +2. **Extract**: `tar -xzf frp-manager-deployment-v1.0.0.tar.gz` +3. **Configure**: + ```bash + cp .env.app.example .env # Configure app environment + cp .env.node.example .env # Configure node environment + ``` +4. **Deploy App** (VPS): + ```bash + docker-compose -f docker-compose-app.yml up -d + ``` +5. **Deploy Node** (Home Server): + ```bash + docker-compose -f docker-compose-node.yml up -d + ``` + +## Monitoring and Troubleshooting + +### 1. **Workflow Status** +- Check workflow runs in the Actions tab +- View build logs for debugging +- Monitor image registry for published images + +### 2. **Common Issues** + +**Build Failures**: +```bash +# Check TypeScript errors +npx tsc --noEmit + +# Check Docker build context +docker build --no-cache ./app +``` + +**Image Push Failures**: +- Verify GITHUB_TOKEN permissions +- Check registry connectivity +- Ensure package publishing is enabled + +**Deployment Issues**: +- Verify environment variables +- Check image availability in registry +- Validate Docker Compose syntax + +### 3. **Local Testing** + +Test workflows locally before pushing: + +```bash +# Test app build +cd app && npm run build + +# Test node build +cd node && bun run build + +# Test Docker builds +docker build -t test-app ./app +docker build -t test-node ./node +``` + +## Customization + +### 1. **Modify Triggers** +Edit workflow files to change when builds occur: +```yaml +on: + push: + branches: [ main, develop, feature/* ] # Add feature branches + paths: + - 'app/**' + - 'docs/**' # Add documentation changes +``` + +### 2. **Add Tests** +Extend test workflows with actual test suites: +```yaml +- name: Run unit tests + run: npm test + +- name: Run integration tests + run: npm run test:integration +``` + +### 3. **Custom Deployment** +Add deployment to your specific infrastructure: +```yaml +- name: Deploy to Kubernetes + run: kubectl apply -f k8s/ + +- name: Deploy via SSH + run: | + scp deployment.tar.gz user@server:/tmp/ + ssh user@server 'cd /tmp && ./deploy.sh' +``` + +## Best Practices + +### 1. **Branch Strategy** +- Use `develop` for integration testing +- Use `main` for production-ready code +- Create releases for versioned deployments + +### 2. **Image Management** +- Tag releases with semantic versions +- Use `latest` tag sparingly +- Clean up old images periodically + +### 3. **Security** +- Regularly update base images +- Monitor security scan results +- Use specific version tags in production + +### 4. **Performance** +- Leverage build caching +- Use multi-stage Docker builds +- Optimize image sizes + +This CI/CD setup provides a robust foundation for building, testing, and deploying the FRP Manager application with proper automation and quality gates. diff --git a/NODE_INTEGRATION.md b/NODE_INTEGRATION.md new file mode 100644 index 0000000..1977885 --- /dev/null +++ b/NODE_INTEGRATION.md @@ -0,0 +1,229 @@ +# FRP Manager - Node Integration Setup + +This guide explains how to set up the integration between the FRP Manager app and the home server node. + +## Overview + +The integration allows the FRP Manager app (running on your VPS) to: +- Query the home server node for status +- Send updated frpc.toml configurations +- Restart the FRP client remotely +- Monitor node connectivity + +## Setup Instructions + +### 1. Environment Configuration + +#### App (.env file) +```bash +# Copy from .env.example and configure these variables: + +# FRP Server Configuration +FRPC_SERVER_ADDR=your-vps-ip-address +FRPC_SERVER_PORT=7000 +FRPC_TOKEN=your-secret-token + +# Node Integration +NODE_URL=http://your-home-server-ip:3001 +NODE_TOKEN=your-node-secret-token +NODE_TIMEOUT=5000 +``` + +#### Node (.env file) +```bash +# Copy from .env.example and configure these variables: + +# Authentication +API_TOKEN=your-node-secret-token + +# FRP Configuration +FRPC_CONFIG_PATH=/app/data/frpc.toml +FRPC_CONTAINER_NAME=frpc +``` + +### 2. Security Considerations + +**Important**: Use the same token for `NODE_TOKEN` in the app and `API_TOKEN` in the node for authentication. + +- Generate a strong, random token (e.g., 32+ character string) +- Keep tokens secure and never commit them to version control +- Use HTTPS in production for encrypted communication + +### 3. Network Setup + +#### Port Configuration +- **App**: Runs on port 3000 (configurable) +- **Node**: Runs on port 3001 (configurable) + +#### Firewall Rules +Ensure the node port (3001) is accessible from your VPS: +```bash +# On your home server (if using UFW) +sudo ufw allow 3001/tcp +``` + +#### Network Access +- The app needs HTTP/HTTPS access to the node +- Consider using a VPN or port forwarding if the node is behind NAT +- For production, use HTTPS with proper certificates + +### 4. Docker Deployment + +#### Starting the App (VPS) +```bash +cd app/ +cp .env.example .env +# Edit .env with your configuration +docker-compose up -d +``` + +#### Starting the Node (Home Server) +```bash +cd node/ +cp .env.example .env +# Edit .env with your configuration +docker-compose up -d +``` + +### 5. API Endpoints + +#### Node Endpoints (Protected by API_TOKEN) +- `GET /api/status` - Get server status +- `GET /api/frpc/status` - Get frpc container status +- `POST /api/frpc/update-config` - Update frpc configuration +- `POST /api/frpc/restart` - Restart frpc container +- `POST /api/frpc/push-and-restart` - Update config and restart in one call + +#### App Endpoints (Node Integration) +- `GET /api/node/status` - Get node status through app +- `GET /api/node/connection` - Get node connection info +- `POST /api/node/push-config` - Push current config to node +- `POST /api/node/restart-frpc` - Restart frpc on node +- `POST /api/node/push-and-restart` - Push config and restart frpc on node + +### 6. Frontend Features + +#### Dashboard +- Live node status indicator +- Connection monitoring +- Last connection time tracking + +#### Tunnel Manager +- "Push to Node" button for deploying configurations +- Real-time node connectivity status +- Error handling and user feedback + +### 7. Testing the Integration + +#### 1. Verify Node Connectivity +```bash +# From your VPS, test the node endpoint +curl -H "Authorization: Bearer your-node-secret-token" \ + http://your-home-server-ip:3001/health +``` + +#### 2. Test Configuration Push +1. Create/modify tunnels in the app +2. Click "Push to Node" button +3. Verify configuration updated on the node +4. Check that frpc restarted successfully + +#### 3. Monitor Logs +```bash +# App logs +docker logs frp-manager + +# Node logs +docker logs home-server-agent + +# FRP client logs +docker logs frpc +``` + +### 8. Troubleshooting + +#### Common Issues + +**"Node client not configured"** +- Check that `NODE_URL` and `NODE_TOKEN` are set in app environment +- Verify environment variables are loaded correctly + +**"Failed to connect to node"** +- Verify node is running and accessible +- Check firewall rules +- Ensure correct IP address and port +- Verify token authentication + +**"frpc container not found"** +- Ensure frpc container exists with the correct name +- Check `FRPC_CONTAINER_NAME` environment variable +- Verify Docker is accessible from the node + +#### Debugging Steps + +1. **Check Environment Variables** + ```bash + # In app container + docker exec frp-manager env | grep NODE_ + + # In node container + docker exec home-server-agent env | grep API_TOKEN + ``` + +2. **Test Direct API Calls** + ```bash + # Test node health endpoint (no auth required) + curl http://your-home-server-ip:3001/health + + # Test authenticated endpoint + curl -H "Authorization: Bearer your-token" \ + http://your-home-server-ip:3001/api/status + ``` + +3. **Check Container Status** + ```bash + # On home server + docker ps | grep frpc + docker logs frpc + ``` + +### 9. Production Considerations + +#### Security +- Use HTTPS with valid certificates +- Implement IP whitelisting if possible +- Regular token rotation +- Monitor authentication logs + +#### Monitoring +- Set up health checks for both app and node +- Monitor node connectivity from app +- Log all configuration changes +- Set up alerts for connection failures + +#### Backup +- Backup frpc configurations +- Backup tunnel database +- Document recovery procedures + +## Usage Examples + +### Deploying New Tunnel Configuration + +1. **Add/Edit Tunnels**: Use the web interface to create or modify tunnel configurations +2. **Push to Node**: Click the "Push to Node" button in the Tunnel Manager +3. **Verify**: Check the dashboard for node status and tunnel activity + +### Manual Configuration Management + +```bash +# Push configuration via API +curl -X POST -H "Authorization: Bearer app-token" \ + http://your-vps:3000/api/node/push-config + +# Restart frpc on node +curl -X POST -H "Authorization: Bearer app-token" \ + http://your-vps:3000/api/node/restart-frpc +``` + +This integration provides a seamless way to manage FRP configurations across your infrastructure while maintaining security and monitoring capabilities. diff --git a/app/.env b/app/.env new file mode 100644 index 0000000..8bec964 --- /dev/null +++ b/app/.env @@ -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 diff --git a/app/.env.example b/app/.env.example new file mode 100644 index 0000000..8366528 --- /dev/null +++ b/app/.env.example @@ -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 diff --git a/app/.gitignore b/app/.gitignore new file mode 100644 index 0000000..a547bf3 --- /dev/null +++ b/app/.gitignore @@ -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? diff --git a/app/DEPLOYMENT.md b/app/DEPLOYMENT.md new file mode 100644 index 0000000..5b9fa31 --- /dev/null +++ b/app/DEPLOYMENT.md @@ -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 + 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` diff --git a/app/Dockerfile b/app/Dockerfile new file mode 100644 index 0000000..02eb8da --- /dev/null +++ b/app/Dockerfile @@ -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"] diff --git a/app/README.md b/app/README.md new file mode 100644 index 0000000..b2f59d7 --- /dev/null +++ b/app/README.md @@ -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 +- � **Push to Node**: Deploy configurations to remote nodes with one click +- 🌐 **Live Monitoring**: Real-time node connectivity and status tracking +- �🐳 **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 +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 diff --git a/app/bun.lock b/app/bun.lock new file mode 100644 index 0000000..9a7d846 --- /dev/null +++ b/app/bun.lock @@ -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=="], + } +} diff --git a/app/data/frpc.toml b/app/data/frpc.toml new file mode 100644 index 0000000..376bfd4 --- /dev/null +++ b/app/data/frpc.toml @@ -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 diff --git a/app/data/tunnels.db b/app/data/tunnels.db new file mode 100644 index 0000000000000000000000000000000000000000..4527a1c2a091ccde475b55873b21c0c510dfe980 GIT binary patch literal 16384 zcmeI#&2F1O5C?E@tR{+NIkz0R%+0oxSW0uOsutsogM!xtFLHB2)X)eC8<*g!*S1nG zeS^MHAEj^5V*xKlb(OZaO4ffRKGrikW`DaJnBH1Z7Sfv}UHBPYDkqAnDldr?MQP+m zGe4@f+gGc=AFd6h|L#kx{X=QC+Dhwt`&;{KeuD-92tWV=5P$##AOHafK;Zuie3&&4 zwHMmaM=wjG`};6muL3^{y==W)hVg^9=?0#k$zKeI>4K4P%+ZKB^l17ql}*1Ad6sJr zi$N1kvWkoj*vRDao~kUFLQ^JW`Qq>CIT^0wFfKe{j@eY`W1D8z%wZG-d62VNKMJ17 zVvG+z_0_{uO;bN#X8!9qlvU-WQB6Z1Sgd*|>TVw;FDck7LAGO#b?1(5*xlJSxbzp{ zCd?;-xNBMDa`V<@b;>GDvSgmbJ88x>Gj6)upvkG*5r*A4qmDbYJH1|AAx`Fg>_w|z zE9}NsNt%(#1uNE~dYU>Z4Hrqin19I%mjzHjgUQ5VI;SDK(p^i?MO|o~hWV~2*P(Q& zQhr;KdPu!^00Izz p00bZa0SG_<0uU$#@coaC009U<00Izz00bZa0SG_<0ub1LfuGlF;~M|~ literal 0 HcmV?d00001 diff --git a/app/docker-compose.override.yml b/app/docker-compose.override.yml new file mode 100644 index 0000000..b7eaff9 --- /dev/null +++ b/app/docker-compose.override.yml @@ -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" diff --git a/app/docker-compose.yml b/app/docker-compose.yml new file mode 100644 index 0000000..627c43c --- /dev/null +++ b/app/docker-compose.yml @@ -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: diff --git a/app/health-check.ps1 b/app/health-check.ps1 new file mode 100644 index 0000000..c21553d --- /dev/null +++ b/app/health-check.ps1 @@ -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 diff --git a/app/health-check.sh b/app/health-check.sh new file mode 100644 index 0000000..c109a83 --- /dev/null +++ b/app/health-check.sh @@ -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" diff --git a/app/index.html b/app/index.html new file mode 100644 index 0000000..110f81d --- /dev/null +++ b/app/index.html @@ -0,0 +1,13 @@ + + + + + + + Vite + React + TS + + +
+ + + diff --git a/app/package.json b/app/package.json new file mode 100644 index 0000000..fedbf2b --- /dev/null +++ b/app/package.json @@ -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" + } +} \ No newline at end of file diff --git a/app/public/vite.svg b/app/public/vite.svg new file mode 100644 index 0000000..e7b8dfb --- /dev/null +++ b/app/public/vite.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/client/App.css b/app/src/client/App.css new file mode 100644 index 0000000..6451fa4 --- /dev/null +++ b/app/src/client/App.css @@ -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; + } +} diff --git a/app/src/client/App.tsx b/app/src/client/App.tsx new file mode 100644 index 0000000..a69d714 --- /dev/null +++ b/app/src/client/App.tsx @@ -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 ( + + +
+ +
+ + } /> + } /> + } /> + +
+ +
+
+ +
+ ); +} + +export default App; diff --git a/app/src/client/api/client.ts b/app/src/client/api/client.ts new file mode 100644 index 0000000..3d8bfbc --- /dev/null +++ b/app/src/client/api/client.ts @@ -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 => { + const response = await apiClient.get('/tunnels'); + return response.data; + }, + + // Get tunnel by ID + getTunnelById: async (id: string): Promise => { + const response = await apiClient.get(`/tunnels/${id}`); + return response.data; + }, + + // Create new tunnel + createTunnel: async (tunnel: Omit): Promise => { + const response = await apiClient.post('/tunnels', tunnel); + return response.data; + }, + + // Update tunnel + updateTunnel: async (id: string, tunnel: Partial): Promise => { + const response = await apiClient.put(`/tunnels/${id}`, tunnel); + return response.data; + }, + + // Delete tunnel + deleteTunnel: async (id: string): Promise => { + await apiClient.delete(`/tunnels/${id}`); + }, + + // Get tunnel status + getTunnelStatus: async (id: string): Promise => { + const response = await apiClient.get(`/tunnels/${id}/status`); + return response.data; + }, + + // Get all tunnel statuses + getAllTunnelStatuses: async (): Promise => { + const response = await apiClient.get('/tunnels-status'); + return response.data; + }, +}; + +export const frpcApi = { + // Get frpc status + getStatus: async (): Promise => { + 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 => { + const response = await apiClient.get(`/frpc/logs?lines=${lines}`); + return response.data; + }, +}; + +export const nodeApi = { + // Get node status + getStatus: async (): Promise => { + const response = await apiClient.get('/node/status'); + return response.data; + }, + + // Get node connection info + getConnection: async (): Promise => { + 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; diff --git a/app/src/client/assets/react.svg b/app/src/client/assets/react.svg new file mode 100644 index 0000000..6c87de9 --- /dev/null +++ b/app/src/client/assets/react.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/app/src/client/components/Navbar.tsx b/app/src/client/components/Navbar.tsx new file mode 100644 index 0000000..1f97a5d --- /dev/null +++ b/app/src/client/components/Navbar.tsx @@ -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 ( + + ); +}; + +export default Navbar; diff --git a/app/src/client/components/TunnelForm.tsx b/app/src/client/components/TunnelForm.tsx new file mode 100644 index 0000000..ac9f19f --- /dev/null +++ b/app/src/client/components/TunnelForm.tsx @@ -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 = ({ 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 }) => + 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) => { + 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 ( +
+
+
+

{isEditing ? 'Edit Tunnel' : 'Create New Tunnel'}

+ +
+ +
+
+ + +
+ +
+ + +
+ +
+
+ + +
+ +
+ + +
+
+ +
+ + +
+ +
+ +
+ +
+ + +
+
+
+
+ ); +}; + +export default TunnelForm; diff --git a/app/src/client/index.css b/app/src/client/index.css new file mode 100644 index 0000000..2c3fac6 --- /dev/null +++ b/app/src/client/index.css @@ -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; + } +} diff --git a/app/src/client/main.tsx b/app/src/client/main.tsx new file mode 100644 index 0000000..f95159a --- /dev/null +++ b/app/src/client/main.tsx @@ -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( + + + , +); diff --git a/app/src/client/pages/Dashboard.tsx b/app/src/client/pages/Dashboard.tsx new file mode 100644 index 0000000..76885ad --- /dev/null +++ b/app/src/client/pages/Dashboard.tsx @@ -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 ( +
+
+ + Loading dashboard... +
+
+ ); + } + + return ( +
+
+

FRP Manager Dashboard

+

Manage your tunnel configurations and monitor their status

+
+ +
+
+
+ +
+
+

Active Tunnels

+

{activeTunnelStatuses.length}

+

Currently running

+
+
+ +
+
+ +
+
+

Total Tunnels

+

{tunnels?.length || 0}

+

Configured tunnels

+
+
+ +
+
+ {frpcStatus?.running ? ( + + ) : ( + + )} +
+
+

FRPC Service

+

{frpcStatus?.running ? 'Running' : 'Stopped'}

+

Service status

+
+
+ +
+
+ +
+
+

Enabled Tunnels

+

{activeTunnels.length}

+

Ready to connect

+
+
+ +
+
+ {nodeConnection?.isReachable ? ( + + ) : ( + + )} +
+
+

Node Status

+

{nodeConnection?.isReachable ? 'Online' : 'Offline'}

+

Home server agent

+
+
+
+ +
+
+

Recent Tunnels

+
+ {activeTunnels.slice(0, 5).map(tunnel => ( +
+
+

{tunnel.name}

+

{tunnel.protocol} • {tunnel.localIp}:{tunnel.localPort} → :{tunnel.remotePort}

+
+
+ {tunnelStatuses?.find(s => s.id === tunnel.id)?.active ? ( + Active + ) : ( + Inactive + )} +
+
+ ))} + {activeTunnels.length === 0 && ( +

No active tunnels configured

+ )} +
+
+ +
+

System Status

+
+
+
+ {frpcStatus?.running ? ( + + ) : ( + + )} +
+
+

FRPC Service

+

{frpcStatus?.running ? 'Service is running normally' : 'Service is stopped'}

+
+
+ +
+
+ +
+
+

API Server

+

API is responding normally

+
+
+ +
+
+ +
+
+

Database

+

Database connection is healthy

+
+
+ +
+
+ {nodeConnection?.isReachable ? ( + + ) : ( + + )} +
+
+

Home Server Node

+

+ {nodeConnection?.isReachable + ? `Connected • Last seen: ${nodeConnection.lastConnectionTime ? new Date(nodeConnection.lastConnectionTime).toLocaleTimeString() : 'Now'}` + : 'Disconnected or unreachable' + } +

+
+
+
+
+
+
+ ); +}; + +export default Dashboard; diff --git a/app/src/client/pages/ServerStatus.tsx b/app/src/client/pages/ServerStatus.tsx new file mode 100644 index 0000000..a006dbd --- /dev/null +++ b/app/src/client/pages/ServerStatus.tsx @@ -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 ( +
+
+

Server Status

+
+
+ + + {frpcStatus?.running ? 'Running' : 'Stopped'} + +
+
+
+ +
+
+

Service Controls

+
+ + + + +
+
+ +
+

Service Information

+
+
+ Status: + + {frpcStatus?.running ? 'Running' : 'Stopped'} + +
+
+ Container: + frpc +
+
+ Last Updated: + + {new Date().toLocaleString()} + +
+
+
+
+ +
+
+

+ + Service Logs +

+
+ + +
+
+ +
+ {logsLoading ? ( +
+ + Loading logs... +
+ ) : ( +
+              {logs?.logs || 'No logs available'}
+            
+ )} +
+
+ +
+

System Information

+
+
+
+ +

FRPC Service

+
+
+

Fast Reverse Proxy Client for tunneling services

+

+ Status: + {frpcStatus?.running ? 'Active' : 'Inactive'} + +

+
+
+ +
+
+ +

API Server

+
+
+

RESTful API for managing tunnel configurations

+

+ Status: Running +

+
+
+ +
+
+ +

Configuration

+
+
+

Tunnel configurations stored in SQLite database

+

+ Auto-generated FRPC config from active tunnels +

+
+
+
+
+
+ ); +}; + +export default ServerStatus; diff --git a/app/src/client/pages/TunnelManager.tsx b/app/src/client/pages/TunnelManager.tsx new file mode 100644 index 0000000..374eda7 --- /dev/null +++ b/app/src/client/pages/TunnelManager.tsx @@ -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(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 }) => + 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 ( +
+
Loading tunnels...
+
+ ); + } + + return ( +
+
+

Tunnel Manager

+
+ {nodeConnection && ( +
+ + Node: {nodeConnection.isReachable ? 'Online' : 'Offline'} + + +
+ )} + +
+
+ +
+ {tunnels?.map(tunnel => { + const status = tunnelStatuses?.find(s => s.id === tunnel.id); + return ( +
+
+
+

{tunnel.name}

+
+ + {tunnel.enabled ? 'Enabled' : 'Disabled'} + + + {status?.active ? 'Active' : 'Inactive'} + +
+
+
+ + + +
+
+ +
+
+ Protocol: + {tunnel.protocol} +
+
+ Local: + {tunnel.localIp}:{tunnel.localPort} +
+
+ Remote: + :{tunnel.remotePort} +
+
+ Created: + + {tunnel.createdAt ? new Date(tunnel.createdAt).toLocaleDateString() : 'N/A'} + +
+
+ + {status?.error && ( +
+ Error: {status.error} +
+ )} +
+ ); + })} +
+ + {tunnels?.length === 0 && ( +
+ +

No tunnels configured

+

Get started by adding your first tunnel configuration

+ +
+ )} + + {showForm && ( + + )} +
+ ); +}; + +export default TunnelManager; diff --git a/app/src/client/tsconfig.json b/app/src/client/tsconfig.json new file mode 100644 index 0000000..fa6f55c --- /dev/null +++ b/app/src/client/tsconfig.json @@ -0,0 +1,8 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "module": "ESNext", + "moduleResolution": "Bundler", + "jsx": "react-jsx" + } +} diff --git a/app/src/client/vite-env.d.ts b/app/src/client/vite-env.d.ts new file mode 100644 index 0000000..11f02fe --- /dev/null +++ b/app/src/client/vite-env.d.ts @@ -0,0 +1 @@ +/// diff --git a/app/src/server/database.ts b/app/src/server/database.ts new file mode 100644 index 0000000..d4e0911 --- /dev/null +++ b/app/src/server/database.ts @@ -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 { + 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 | 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(); + } +} diff --git a/app/src/server/frpc-manager.ts b/app/src/server/frpc-manager.ts new file mode 100644 index 0000000..6bd4c2a --- /dev/null +++ b/app/src/server/frpc-manager.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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 { + 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; + } + } +} diff --git a/app/src/server/logger.ts b/app/src/server/logger.ts new file mode 100644 index 0000000..ea82bb5 --- /dev/null +++ b/app/src/server/logger.ts @@ -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; diff --git a/app/src/server/main.ts b/app/src/server/main.ts new file mode 100644 index 0000000..b2c2f25 --- /dev/null +++ b/app/src/server/main.ts @@ -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}...`); +}); diff --git a/app/src/server/node-client.ts b/app/src/server/node-client.ts new file mode 100644 index 0000000..c5fff17 --- /dev/null +++ b/app/src/server/node-client.ts @@ -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( + method: 'GET' | 'POST' | 'PUT' | 'DELETE', + endpoint: string, + data?: any + ): Promise { + try { + const response: AxiosResponse = 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 { + return this.makeRequest('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 { + try { + await this.makeRequest('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'), + }); +} diff --git a/app/src/server/routes.ts b/app/src/server/routes.ts new file mode 100644 index 0000000..96cc0f0 --- /dev/null +++ b/app/src/server/routes.ts @@ -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 | 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) => { + 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; diff --git a/app/src/server/types.ts b/app/src/server/types.ts new file mode 100644 index 0000000..bf77d81 --- /dev/null +++ b/app/src/server/types.ts @@ -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; +export type TunnelConfigUpdate = z.infer; + +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; + }; + }; +} diff --git a/app/tsconfig.json b/app/tsconfig.json new file mode 100644 index 0000000..74c1f05 --- /dev/null +++ b/app/tsconfig.json @@ -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"] +} diff --git a/app/vite.config.ts b/app/vite.config.ts new file mode 100644 index 0000000..9cc50ea --- /dev/null +++ b/app/vite.config.ts @@ -0,0 +1,7 @@ +import { defineConfig } from "vite"; +import react from "@vitejs/plugin-react"; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], +}); diff --git a/node/.dockerignore b/node/.dockerignore new file mode 100644 index 0000000..b77d3b0 --- /dev/null +++ b/node/.dockerignore @@ -0,0 +1,15 @@ +node_modules +npm-debug.log +dist +.git +.gitignore +README.md +.env +.env.example +.nyc_output +coverage +.vscode +logs +*.log +.DS_Store +Thumbs.db diff --git a/node/.env.example b/node/.env.example new file mode 100644 index 0000000..e212827 --- /dev/null +++ b/node/.env.example @@ -0,0 +1,26 @@ +# Home Server Agent Configuration + +# Server Configuration +PORT=3000 +NODE_ENV=development + +# Authentication (set a strong token for production) +API_TOKEN=your-secret-token-here + +# Logging +LOG_LEVEL=info + +# Docker Configuration +DOCKER_HOST=unix:///var/run/docker.sock + +# FRP Configuration +FRPC_CONFIG_PATH=/app/data/frpc.toml +FRPC_CONTAINER_NAME=frpc + +# Game Server Defaults +MINECRAFT_MEMORY=2G +VALHEIM_SERVER_NAME=My Valheim Server +VALHEIM_WORLD_NAME=MyWorld +VALHEIM_SERVER_PASS=secret123 +TERRARIA_WORLD=MyWorld +TERRARIA_PASSWORD=secret123 diff --git a/node/.gitignore b/node/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/node/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/node/DEPLOYMENT.md b/node/DEPLOYMENT.md new file mode 100644 index 0000000..419daf1 --- /dev/null +++ b/node/DEPLOYMENT.md @@ -0,0 +1,380 @@ +# Home Server Agent Deployment Guide + +This guide will help you deploy the Home Server Agent on your home server with Docker. + +## Prerequisites + +- Docker and Docker Compose installed on your home server +- At least 4GB of RAM (for running game servers) +- Open ports for the game servers you want to run +- Basic understanding of Docker and networking + +## Quick Deployment + +### 1. Clone and Setup + +```bash +# Clone the repository +git clone +cd home-server-agent + +# Create environment configuration +cp .env.example .env + +# Edit the .env file with your settings +nano .env +``` + +### 2. Configure Environment + +Edit `.env` file: + +```bash +# IMPORTANT: Change this to a secure token +API_TOKEN=your-very-secure-token-here + +# Server configuration +PORT=3000 +NODE_ENV=production +LOG_LEVEL=info + +# Docker socket (usually default) +DOCKER_HOST=unix:///var/run/docker.sock + +# Game server settings (optional) +MINECRAFT_MEMORY=2G +VALHEIM_SERVER_NAME=Your Valheim Server +VALHEIM_WORLD_NAME=YourWorld +VALHEIM_SERVER_PASS=your-secure-password +``` + +### 3. Deploy with Docker Compose + +```bash +# Start all services (agent + game servers) +docker-compose up -d + +# Or start just the agent for testing +docker-compose -f docker-compose.dev.yml up -d + +# Check status +docker-compose ps +``` + +## Port Configuration + +The following ports will be used: + +| Service | Port | Protocol | Purpose | +|---------|------|----------|---------| +| Agent API | 3000 | TCP | REST API | +| Minecraft | 25565 | TCP | Minecraft server | +| Valheim | 2456-2457 | UDP | Valheim server | +| Terraria | 7777 | TCP | Terraria server | +| Portainer | 9000 | TCP | Docker management UI | + +### Firewall Configuration + +**Ubuntu/Debian:** +```bash +# Allow agent API +sudo ufw allow 3000/tcp + +# Allow game server ports +sudo ufw allow 25565/tcp # Minecraft +sudo ufw allow 2456:2457/udp # Valheim +sudo ufw allow 7777/tcp # Terraria + +# Optional: Portainer +sudo ufw allow 9000/tcp +``` + +**CentOS/RHEL:** +```bash +# Allow agent API +sudo firewall-cmd --permanent --add-port=3000/tcp + +# Allow game server ports +sudo firewall-cmd --permanent --add-port=25565/tcp +sudo firewall-cmd --permanent --add-port=2456-2457/udp +sudo firewall-cmd --permanent --add-port=7777/tcp + +sudo firewall-cmd --reload +``` + +## VPS Integration + +To allow your VPS to control the home server agent: + +### 1. Secure the API Token + +Use a strong, unique API token: + +```bash +# Generate a secure token +openssl rand -hex 32 + +# Or use UUID +uuidgen +``` + +### 2. VPS Configuration + +On your VPS app, configure it to make requests to your home server: + +```javascript +// Example VPS integration +const HOME_SERVER_URL = 'https://your-home-server.com:3000'; // Use HTTPS in production +const API_TOKEN = 'your-secure-token-here'; + +// Start a game server from VPS +const startGameServer = async (serverName) => { + const response = await fetch(`${HOME_SERVER_URL}/api/gameserver/start/${serverName}`, { + method: 'POST', + headers: { + 'Authorization': `Bearer ${API_TOKEN}`, + 'Content-Type': 'application/json' + } + }); + + return response.json(); +}; +``` + +### 3. Network Setup + +**Option A: Port Forwarding** +- Forward port 3000 on your router to your home server +- Consider using a non-standard port for security + +**Option B: VPN/Tunnel** +- Use a VPN to connect your VPS to your home network +- More secure but requires additional setup + +**Option C: Reverse Proxy** +- Use nginx or Apache to proxy requests +- Can add SSL termination and additional security + +## SSL/HTTPS Setup + +For production use, enable HTTPS: + +### Using nginx as reverse proxy: + +```nginx +# /etc/nginx/sites-available/home-server-agent +server { + listen 443 ssl; + server_name your-domain.com; + + ssl_certificate /path/to/your/certificate.pem; + ssl_certificate_key /path/to/your/private.key; + + location / { + proxy_pass http://localhost:3000; + proxy_set_header Host $host; + proxy_set_header X-Real-IP $remote_addr; + proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; + proxy_set_header X-Forwarded-Proto $scheme; + } +} +``` + +## Monitoring and Management + +### Docker Management with Portainer + +```bash +# Enable Portainer +docker-compose --profile management up -d portainer + +# Access at http://your-server:9000 +``` + +### Log Management + +```bash +# View agent logs +docker-compose logs -f home-server-agent + +# View specific game server logs +docker-compose logs -f minecraft + +# View all logs +docker-compose logs -f +``` + +### Health Monitoring + +Set up monitoring with the included health check scripts: + +```bash +# Linux health check +./health-check.sh health + +# Windows health check +./health-check.ps1 -CheckType health + +# Check specific game server +./health-check.sh gameserver minecraft +``` + +## Automatic Updates + +Enable Watchtower for automatic updates: + +```bash +# Enable Watchtower +docker-compose --profile management up -d watchtower + +# Or manually update +docker-compose pull +docker-compose up -d +``` + +## Backup Strategy + +### Game Data Backup + +```bash +# Backup game data volumes +docker run --rm -v minecraft-data:/data -v $(pwd):/backup ubuntu tar czf /backup/minecraft-backup.tar.gz /data + +# Restore game data +docker run --rm -v minecraft-data:/data -v $(pwd):/backup ubuntu tar xzf /backup/minecraft-backup.tar.gz -C / +``` + +### Configuration Backup + +```bash +# Backup configuration +cp .env .env.backup +cp docker-compose.yml docker-compose.yml.backup + +# Backup logs +tar czf logs-backup.tar.gz logs/ +``` + +## Security Checklist + +- [ ] Changed default API token +- [ ] Configured firewall rules +- [ ] Enabled SSL/HTTPS (production) +- [ ] Restricted Docker socket access +- [ ] Set up log rotation +- [ ] Configured backup strategy +- [ ] Tested health monitoring +- [ ] Documented port assignments + +## Troubleshooting + +### Common Issues + +**1. Permission Denied (Docker Socket)** +```bash +# Add user to docker group +sudo usermod -aG docker $USER + +# Or adjust socket permissions +sudo chmod 666 /var/run/docker.sock +``` + +**2. Port Already in Use** +```bash +# Check what's using the port +sudo netstat -tlnp | grep :3000 + +# Stop conflicting service +sudo systemctl stop +``` + +**3. Game Server Won't Start** +```bash +# Check Docker logs +docker logs gameserver-minecraft + +# Check system resources +docker system df +free -h +``` + +**4. API Not Responding** +```bash +# Check agent status +docker-compose ps + +# Check agent logs +docker-compose logs home-server-agent + +# Test local connectivity +curl http://localhost:3000/health +``` + +### Log Analysis + +```bash +# Follow all logs +docker-compose logs -f + +# Search for errors +docker-compose logs | grep -i error + +# Check specific timeframe +docker-compose logs --since=1h +``` + +## Performance Optimization + +### Resource Limits + +Add resource limits to `docker-compose.yml`: + +```yaml +services: + minecraft: + deploy: + resources: + limits: + memory: 2G + cpus: '2.0' + reservations: + memory: 1G + cpus: '1.0' +``` + +### System Requirements + +**Minimum:** +- 2GB RAM +- 2 CPU cores +- 20GB storage + +**Recommended:** +- 8GB RAM +- 4 CPU cores +- 100GB storage +- SSD storage for better performance + +## Production Checklist + +Before deploying to production: + +- [ ] Secure API token configured +- [ ] Firewall rules applied +- [ ] SSL/HTTPS enabled +- [ ] Monitoring configured +- [ ] Backup strategy implemented +- [ ] Resource limits set +- [ ] Documentation updated +- [ ] Testing completed +- [ ] Network security reviewed +- [ ] Access controls verified + +## Support + +For issues: +1. Check the troubleshooting section +2. Review logs for error messages +3. Check Docker and system status +4. Verify network connectivity +5. Open an issue with detailed information diff --git a/node/Dockerfile b/node/Dockerfile new file mode 100644 index 0000000..18e57b7 --- /dev/null +++ b/node/Dockerfile @@ -0,0 +1,56 @@ +# Use the official Bun image as base +FROM oven/bun:1 as builder + +# Set working directory +WORKDIR /app + +# Copy package files +COPY package*.json ./ +COPY bun.lockb* ./ + +# Install dependencies +RUN bun install --frozen-lockfile + +# Copy source code +COPY src/ ./src/ +COPY tsconfig.json ./ + +# Build the application +RUN bun run build + +# Production stage +FROM node:18-alpine + +# Install security updates +RUN apk update && apk upgrade && apk add --no-cache dumb-init + +# Create app directory +WORKDIR /app + +# Create non-root user +RUN addgroup -g 1001 -S nodejs && \ + adduser -S nodeuser -u 1001 + +# Copy built application from builder stage +COPY --from=builder --chown=nodeuser:nodejs /app/dist ./dist +COPY --from=builder --chown=nodeuser:nodejs /app/package.json ./ + +# Install production dependencies +RUN npm ci --only=production && npm cache clean --force + +# Create logs directory +RUN mkdir -p logs && chown nodeuser:nodejs logs + +# Switch to non-root user +USER nodeuser + +# Expose port +EXPOSE 3000 + +# Health check +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD node -e "require('http').get('http://localhost:3000/health', (res) => process.exit(res.statusCode === 200 ? 0 : 1))" + +# Start the application +ENTRYPOINT ["dumb-init", "--"] +CMD ["node", "dist/index.js"] diff --git a/node/README.md b/node/README.md new file mode 100644 index 0000000..8fe8570 --- /dev/null +++ b/node/README.md @@ -0,0 +1,285 @@ +# node + +# Home Server Agent + +A lightweight Node.js agent for managing game servers on your home server through Docker containers. This agent provides a secure REST API to start, stop, and monitor game servers remotely. + +## Features + +- 🎮 **Game Server Management**: Start, stop, and monitor popular game servers (Minecraft, Valheim, Terraria) +- 🐳 **Docker Integration**: Leverages Docker for containerized game server deployment +- 🔒 **Security**: Token-based authentication for API access +- 📊 **Monitoring**: Real-time status monitoring and system statistics +- 🌐 **REST API**: Clean RESTful API for remote management +- 📝 **Logging**: Comprehensive logging with Winston +- 🔄 **Auto-restart**: Containers automatically restart on failure +- 💾 **Data Persistence**: Game data persisted in Docker volumes + +## Quick Start + +### Prerequisites + +- Docker and Docker Compose installed +- Node.js 18+ (for development) +- Bun (optional, for development) + +### Development Setup + +1. **Clone the repository** + ```bash + git clone + cd home-server-agent + ``` + +2. **Install dependencies** + ```bash + bun install + # or + npm install + ``` + +3. **Set up environment** + ```bash + cp .env.example .env + # Edit .env with your configuration + ``` + +4. **Start development server** + ```bash + bun run dev + # or + npm run dev + ``` + +### Production Deployment + +1. **Build and start with Docker Compose** + ```bash + docker-compose up -d + ``` + +2. **Or start with just the agent for testing** + ```bash + docker-compose -f docker-compose.dev.yml up -d + ``` + +## API Endpoints + +### Authentication + +All API endpoints (except `/health`) require authentication via Bearer token or `X-API-Key` header: + +```bash +# Using Bearer token +curl -H "Authorization: Bearer your-secret-token-here" http://localhost:3000/api/status + +# Using API key header +curl -H "X-API-Key: your-secret-token-here" http://localhost:3000/api/status +``` + +### Available Endpoints + +| Endpoint | Method | Description | +|----------|--------|-------------| +| `/health` | GET | Health check (no auth required) | +| `/api/status` | GET | Get overall server status | +| `/api/status/ports` | GET | Get active ports and services | +| `/api/gameserver/list` | GET | List all available game servers | +| `/api/gameserver/start/:serviceName` | POST | Start a game server | +| `/api/gameserver/stop/:serviceName` | POST | Stop a game server | +| `/api/gameserver/restart/:serviceName` | POST | Restart a game server | +| `/api/gameserver/:serviceName/status` | GET | Get specific server status | + +### Example Usage + +```bash +# Get server status +curl -H "Authorization: Bearer your-secret-token-here" \ + http://localhost:3000/api/status + +# Start Minecraft server +curl -X POST -H "Authorization: Bearer your-secret-token-here" \ + http://localhost:3000/api/gameserver/start/minecraft + +# Stop Minecraft server +curl -X POST -H "Authorization: Bearer your-secret-token-here" \ + http://localhost:3000/api/gameserver/stop/minecraft + +# Get Minecraft server status +curl -H "Authorization: Bearer your-secret-token-here" \ + http://localhost:3000/api/gameserver/minecraft/status +``` + +## Supported Game Servers + +### Minecraft +- **Image**: `itzg/minecraft-server:latest` +- **Default Port**: 25565 +- **Features**: Vanilla Minecraft server with configurable settings + +### Valheim +- **Image**: `lloesche/valheim-server:latest` +- **Default Ports**: 2456-2457 (UDP) +- **Features**: Dedicated Valheim server with world persistence + +### Terraria +- **Image**: `ryshe/terraria:latest` +- **Default Port**: 7777 +- **Features**: Terraria server with world management + +## Configuration + +### Environment Variables + +| Variable | Description | Default | +|----------|-------------|---------| +| `PORT` | Server port | 3000 | +| `NODE_ENV` | Environment mode | development | +| `API_TOKEN` | Authentication token | Required | +| `LOG_LEVEL` | Logging level | info | +| `DOCKER_HOST` | Docker socket path | unix:///var/run/docker.sock | + +### Game Server Configuration + +Game servers can be configured by modifying the environment variables in `docker-compose.yml`: + +```yaml +minecraft: + environment: + EULA: "TRUE" + TYPE: "VANILLA" + MEMORY: "2G" + DIFFICULTY: "normal" + MAX_PLAYERS: "20" +``` + +## Security + +- **Authentication**: All API endpoints require token authentication +- **Docker Security**: Containers run as non-root users where possible +- **Network Isolation**: Services run in isolated Docker networks +- **Read-only Docker Socket**: Docker socket is mounted read-only for security + +## Monitoring + +The agent provides comprehensive monitoring capabilities: + +- **System Stats**: Docker version, container counts, resource usage +- **Port Monitoring**: Active ports and their associated services +- **Container Status**: Real-time container health and uptime +- **Logs**: Structured logging with Winston + +## Development + +### Project Structure + +``` +src/ +├── index.ts # Main application entry point +├── middleware/ +│ └── auth.ts # Authentication middleware +├── routes/ +│ ├── status.ts # Status and monitoring routes +│ └── gameServer.ts # Game server management routes +├── services/ +│ └── dockerManager.ts # Docker container management +└── utils/ + └── logger.ts # Logging configuration +``` + +### Scripts + +```bash +# Development +bun run dev # Start development server with hot reload +bun run build # Build the application +bun run start # Start production server + +# Docker +bun run docker:build # Build Docker image +bun run docker:run # Run with Docker Compose +``` + +## Docker Volumes + +The following volumes are used for data persistence: + +- `minecraft-data`: Minecraft world data and configuration +- `valheim-data`: Valheim world data and configuration +- `terraria-data`: Terraria world data and configuration +- `./logs`: Application logs (mounted from host) + +## Optional Services + +The Docker Compose file includes optional management services: + +### Portainer (Docker Management UI) +```bash +docker-compose --profile management up -d portainer +``` +Access at: http://localhost:9000 + +### Watchtower (Auto-updates) +```bash +docker-compose --profile management up -d watchtower +``` + +## Troubleshooting + +### Common Issues + +1. **Permission Denied (Docker Socket)** + ```bash + # Ensure Docker socket has proper permissions + sudo chmod 666 /var/run/docker.sock + ``` + +2. **Port Already in Use** + ```bash + # Check what's using the port + netstat -tlnp | grep :3000 + ``` + +3. **Container Start Failures** + ```bash + # Check container logs + docker logs gameserver-minecraft + ``` + +### Logs + +Application logs are available in: +- `./logs/combined.log` - All logs +- `./logs/error.log` - Error logs only +- Console output (development mode) + +## License + +This project is licensed under the MIT License. + +## Contributing + +1. Fork the repository +2. Create a feature branch +3. Make your changes +4. Add tests if applicable +5. Submit a pull request + +## Support + +For issues and questions: +1. Check the troubleshooting section +2. Review the logs for error messages +3. Open an issue on GitHub with detailed informationall dependencies: + +```bash +bun install +``` + +To run: + +```bash +bun run index.ts +``` + +This project was created using `bun init` in bun v1.2.6. [Bun](https://bun.sh) is a fast all-in-one JavaScript runtime. diff --git a/node/bun.lock b/node/bun.lock new file mode 100644 index 0000000..a255d9a --- /dev/null +++ b/node/bun.lock @@ -0,0 +1,430 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "node", + "dependencies": { + "cors": "^2.8.5", + "dockerode": "^4.0.2", + "dotenv": "^16.3.1", + "express": "^4.18.2", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "winston": "^3.11.0", + }, + "devDependencies": { + "@types/bun": "latest", + "@types/cors": "^2.8.17", + "@types/express": "^4.17.21", + "@types/morgan": "^1.9.9", + "@types/node": "^20.10.0", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@balena/dockerignore": ["@balena/dockerignore@1.0.2", "", {}, "sha512-wMue2Sy4GAVTk6Ic4tJVcnfdau+gx2EnG7S+uAEe+TWJFqE4YoWN4/H8MSLj4eYJKxGg26lZwboEniNiNwZQ6Q=="], + + "@colors/colors": ["@colors/colors@1.6.0", "", {}, "sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA=="], + + "@dabh/diagnostics": ["@dabh/diagnostics@2.0.3", "", { "dependencies": { "colorspace": "1.1.x", "enabled": "2.0.x", "kuler": "^2.0.0" } }, "sha512-hrlQOIi7hAfzsMqlGSFyVucrx38O+j6wiGOf//H2ecvIEqYN4ADBSS2iLMh5UFyDunCNniUIPk/q3riFv45xRA=="], + + "@grpc/grpc-js": ["@grpc/grpc-js@1.13.4", "", { "dependencies": { "@grpc/proto-loader": "^0.7.13", "@js-sdsl/ordered-map": "^4.4.2" } }, "sha512-GsFaMXCkMqkKIvwCQjCrwH+GHbPKBjhwo/8ZuUkWHqbI73Kky9I+pQltrlT0+MWpedCoosda53lgjYfyEPgxBg=="], + + "@grpc/proto-loader": ["@grpc/proto-loader@0.7.15", "", { "dependencies": { "lodash.camelcase": "^4.3.0", "long": "^5.0.0", "protobufjs": "^7.2.5", "yargs": "^17.7.2" }, "bin": { "proto-loader-gen-types": "build/bin/proto-loader-gen-types.js" } }, "sha512-tMXdRCfYVixjuFK+Hk0Q1s38gV9zDiDJfWL3h1rv4Qc39oILCu1TRTDt7+fGUI8K4G1Fj125Hx/ru3azECWTyQ=="], + + "@js-sdsl/ordered-map": ["@js-sdsl/ordered-map@4.4.2", "", {}, "sha512-iUKgm52T8HOE/makSxjqoWhe95ZJA1/G1sYsGev2JDKUSS14KAgg1LHb+Ba+IPow0xflbnSkOsZcO08C7w1gYw=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@types/body-parser": ["@types/body-parser@1.19.6", "", { "dependencies": { "@types/connect": "*", "@types/node": "*" } }, "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g=="], + + "@types/bun": ["@types/bun@1.2.17", "", { "dependencies": { "bun-types": "1.2.17" } }, "sha512-l/BYs/JYt+cXA/0+wUhulYJB6a6p//GTPiJ7nV+QHa8iiId4HZmnu/3J/SowP5g0rTiERY2kfGKXEK5Ehltx4Q=="], + + "@types/connect": ["@types/connect@3.4.38", "", { "dependencies": { "@types/node": "*" } }, "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug=="], + + "@types/cors": ["@types/cors@2.8.19", "", { "dependencies": { "@types/node": "*" } }, "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg=="], + + "@types/express": ["@types/express@4.17.23", "", { "dependencies": { "@types/body-parser": "*", "@types/express-serve-static-core": "^4.17.33", "@types/qs": "*", "@types/serve-static": "*" } }, "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ=="], + + "@types/express-serve-static-core": ["@types/express-serve-static-core@4.19.6", "", { "dependencies": { "@types/node": "*", "@types/qs": "*", "@types/range-parser": "*", "@types/send": "*" } }, "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A=="], + + "@types/http-errors": ["@types/http-errors@2.0.5", "", {}, "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg=="], + + "@types/mime": ["@types/mime@1.3.5", "", {}, "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w=="], + + "@types/morgan": ["@types/morgan@1.9.10", "", { "dependencies": { "@types/node": "*" } }, "sha512-sS4A1zheMvsADRVfT0lYbJ4S9lmsey8Zo2F7cnbYjWHP67Q0AwMYuuzLlkIM2N8gAbb9cubhIVFwcIN2XyYCkA=="], + + "@types/node": ["@types/node@20.19.4", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-OP+We5WV8Xnbuvw0zC2m4qfB/BJvjyCwtNjhHdJxV1639SGSKrLmJkc3fMnp2Qy8nJyHp8RO6umxELN/dS1/EA=="], + + "@types/qs": ["@types/qs@6.14.0", "", {}, "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ=="], + + "@types/range-parser": ["@types/range-parser@1.2.7", "", {}, "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ=="], + + "@types/send": ["@types/send@0.17.5", "", { "dependencies": { "@types/mime": "^1", "@types/node": "*" } }, "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w=="], + + "@types/serve-static": ["@types/serve-static@1.15.8", "", { "dependencies": { "@types/http-errors": "*", "@types/node": "*", "@types/send": "*" } }, "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg=="], + + "@types/triple-beam": ["@types/triple-beam@1.3.5", "", {}, "sha512-6WaYesThRMCl19iryMYP7/x2OVgCtbIVflDGFpWnb9irXI3UjYE4AzmYuiUKY1AJstGijoY+MgUszMgRxIYTYw=="], + + "accepts": ["accepts@1.3.8", "", { "dependencies": { "mime-types": "~2.1.34", "negotiator": "0.6.3" } }, "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "array-flatten": ["array-flatten@1.1.1", "", {}, "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg=="], + + "asn1": ["asn1@0.2.6", "", { "dependencies": { "safer-buffer": "~2.1.0" } }, "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ=="], + + "async": ["async@3.2.6", "", {}, "sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "basic-auth": ["basic-auth@2.0.1", "", { "dependencies": { "safe-buffer": "5.1.2" } }, "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg=="], + + "bcrypt-pbkdf": ["bcrypt-pbkdf@1.0.2", "", { "dependencies": { "tweetnacl": "^0.14.3" } }, "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w=="], + + "bl": ["bl@4.1.0", "", { "dependencies": { "buffer": "^5.5.0", "inherits": "^2.0.4", "readable-stream": "^3.4.0" } }, "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w=="], + + "body-parser": ["body-parser@1.20.3", "", { "dependencies": { "bytes": "3.1.2", "content-type": "~1.0.5", "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "on-finished": "2.4.1", "qs": "6.13.0", "raw-body": "2.5.2", "type-is": "~1.6.18", "unpipe": "1.0.0" } }, "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g=="], + + "buffer": ["buffer@5.7.1", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.1.13" } }, "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ=="], + + "buildcheck": ["buildcheck@0.0.6", "", {}, "sha512-8f9ZJCUXyT1M35Jx7MkBgmBMo3oHTTBIPLiY9xyL0pl3T5RwcPEY8cUHr5LBNfu/fk6c2T4DJZuVM/8ZZT2D2A=="], + + "bun-types": ["bun-types@1.2.17", "", { "dependencies": { "@types/node": "*" } }, "sha512-ElC7ItwT3SCQwYZDYoAH+q6KT4Fxjl8DtZ6qDulUFBmXA8YB4xo+l54J9ZJN+k2pphfn9vk7kfubeSd5QfTVJQ=="], + + "bytes": ["bytes@3.1.2", "", {}, "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg=="], + + "call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ=="], + + "call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg=="], + + "chownr": ["chownr@1.1.4", "", {}, "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg=="], + + "cliui": ["cliui@8.0.1", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.1", "wrap-ansi": "^7.0.0" } }, "sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ=="], + + "color": ["color@3.2.1", "", { "dependencies": { "color-convert": "^1.9.3", "color-string": "^1.6.0" } }, "sha512-aBl7dZI9ENN6fUGC7mWpMTPNHmWUSNan9tuWN6ahh5ZLNk9baLJOnSMlrQkHcrfFgz2/RigjUVAjdx36VcemKA=="], + + "color-convert": ["color-convert@1.9.3", "", { "dependencies": { "color-name": "1.1.3" } }, "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg=="], + + "color-name": ["color-name@1.1.3", "", {}, "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw=="], + + "color-string": ["color-string@1.9.1", "", { "dependencies": { "color-name": "^1.0.0", "simple-swizzle": "^0.2.2" } }, "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg=="], + + "colorspace": ["colorspace@1.1.4", "", { "dependencies": { "color": "^3.1.3", "text-hex": "1.0.x" } }, "sha512-BgvKJiuVu1igBUF2kEjRCZXol6wiiGbY5ipL/oVPwm0BL9sIpMIzM8IK7vwuxIIzOXMV3Ey5w+vxhm0rR/TN8w=="], + + "content-disposition": ["content-disposition@0.5.4", "", { "dependencies": { "safe-buffer": "5.2.1" } }, "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ=="], + + "content-type": ["content-type@1.0.5", "", {}, "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA=="], + + "cookie": ["cookie@0.7.1", "", {}, "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w=="], + + "cookie-signature": ["cookie-signature@1.0.6", "", {}, "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="], + + "cors": ["cors@2.8.5", "", { "dependencies": { "object-assign": "^4", "vary": "^1" } }, "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g=="], + + "cpu-features": ["cpu-features@0.0.10", "", { "dependencies": { "buildcheck": "~0.0.6", "nan": "^2.19.0" } }, "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA=="], + + "debug": ["debug@2.6.9", "", { "dependencies": { "ms": "2.0.0" } }, "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA=="], + + "depd": ["depd@2.0.0", "", {}, "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw=="], + + "destroy": ["destroy@1.2.0", "", {}, "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg=="], + + "docker-modem": ["docker-modem@5.0.6", "", { "dependencies": { "debug": "^4.1.1", "readable-stream": "^3.5.0", "split-ca": "^1.0.1", "ssh2": "^1.15.0" } }, "sha512-ens7BiayssQz/uAxGzH8zGXCtiV24rRWXdjNha5V4zSOcxmAZsfGVm/PPFbwQdqEkDnhG+SyR9E3zSHUbOKXBQ=="], + + "dockerode": ["dockerode@4.0.7", "", { "dependencies": { "@balena/dockerignore": "^1.0.2", "@grpc/grpc-js": "^1.11.1", "@grpc/proto-loader": "^0.7.13", "docker-modem": "^5.0.6", "protobufjs": "^7.3.2", "tar-fs": "~2.1.2", "uuid": "^10.0.0" } }, "sha512-R+rgrSRTRdU5mH14PZTCPZtW/zw3HDWNTS/1ZAQpL/5Upe/ye5K9WQkIysu4wBoiMwKynsz0a8qWuGsHgEvSAA=="], + + "dotenv": ["dotenv@16.6.1", "", {}, "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow=="], + + "dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A=="], + + "ee-first": ["ee-first@1.1.1", "", {}, "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "enabled": ["enabled@2.0.0", "", {}, "sha512-AKrN98kuwOzMIdAizXGI86UFBoo26CL21UM763y1h/GMSJ4/OHU9k2YlsmBpyScFo/wbLzWQJBMCW4+IO3/+OQ=="], + + "encodeurl": ["encodeurl@2.0.0", "", {}, "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "es-define-property": ["es-define-property@1.0.1", "", {}, "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escape-html": ["escape-html@1.0.3", "", {}, "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="], + + "etag": ["etag@1.8.1", "", {}, "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg=="], + + "express": ["express@4.21.2", "", { "dependencies": { "accepts": "~1.3.8", "array-flatten": "1.1.1", "body-parser": "1.20.3", "content-disposition": "0.5.4", "content-type": "~1.0.4", "cookie": "0.7.1", "cookie-signature": "1.0.6", "debug": "2.6.9", "depd": "2.0.0", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "etag": "~1.8.1", "finalhandler": "1.3.1", "fresh": "0.5.2", "http-errors": "2.0.0", "merge-descriptors": "1.0.3", "methods": "~1.1.2", "on-finished": "2.4.1", "parseurl": "~1.3.3", "path-to-regexp": "0.1.12", "proxy-addr": "~2.0.7", "qs": "6.13.0", "range-parser": "~1.2.1", "safe-buffer": "5.2.1", "send": "0.19.0", "serve-static": "1.16.2", "setprototypeof": "1.2.0", "statuses": "2.0.1", "type-is": "~1.6.18", "utils-merge": "1.0.1", "vary": "~1.1.2" } }, "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA=="], + + "fecha": ["fecha@4.2.3", "", {}, "sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw=="], + + "finalhandler": ["finalhandler@1.3.1", "", { "dependencies": { "debug": "2.6.9", "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "on-finished": "2.4.1", "parseurl": "~1.3.3", "statuses": "2.0.1", "unpipe": "~1.0.0" } }, "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ=="], + + "fn.name": ["fn.name@1.1.0", "", {}, "sha512-GRnmB5gPyJpAhTQdSZTSp9uaPSvl09KoYcMQtsB9rQoOmzs9dH6ffeccH+Z+cv6P68Hu5bC6JjRh4Ah/mHSNRw=="], + + "forwarded": ["forwarded@0.2.0", "", {}, "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="], + + "fresh": ["fresh@0.5.2", "", {}, "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q=="], + + "fs-constants": ["fs-constants@1.0.0", "", {}, "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], + + "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], + + "gopd": ["gopd@1.2.0", "", {}, "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg=="], + + "has-symbols": ["has-symbols@1.1.0", "", {}, "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ=="], + + "hasown": ["hasown@2.0.2", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ=="], + + "helmet": ["helmet@7.2.0", "", {}, "sha512-ZRiwvN089JfMXokizgqEPXsl2Guk094yExfoDXR0cBYWxtBbaSww/w+vT4WEJsBW2iTUi1GgZ6swmoug3Oy4Xw=="], + + "http-errors": ["http-errors@2.0.0", "", { "dependencies": { "depd": "2.0.0", "inherits": "2.0.4", "setprototypeof": "1.2.0", "statuses": "2.0.1", "toidentifier": "1.0.1" } }, "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ=="], + + "iconv-lite": ["iconv-lite@0.4.24", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3" } }, "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "inherits": ["inherits@2.0.4", "", {}, "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ=="], + + "ipaddr.js": ["ipaddr.js@1.9.1", "", {}, "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g=="], + + "is-arrayish": ["is-arrayish@0.3.2", "", {}, "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-stream": ["is-stream@2.0.1", "", {}, "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg=="], + + "kuler": ["kuler@2.0.0", "", {}, "sha512-Xq9nH7KlWZmXAtodXDDRE7vs6DU1gTU8zYDHDiWLSip45Egwq3plLHzPn27NgvzL2r1LMPC1vdqh98sQxtqj4A=="], + + "lodash.camelcase": ["lodash.camelcase@4.3.0", "", {}, "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA=="], + + "logform": ["logform@2.7.0", "", { "dependencies": { "@colors/colors": "1.6.0", "@types/triple-beam": "^1.3.2", "fecha": "^4.2.0", "ms": "^2.1.1", "safe-stable-stringify": "^2.3.1", "triple-beam": "^1.3.0" } }, "sha512-TFYA4jnP7PVbmlBIfhlSe+WKxs9dklXMTEGcBCIvLhE/Tn3H6Gk1norupVW7m5Cnd4bLcr08AytbyV/xj7f/kQ=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], + + "media-typer": ["media-typer@0.3.0", "", {}, "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ=="], + + "merge-descriptors": ["merge-descriptors@1.0.3", "", {}, "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ=="], + + "methods": ["methods@1.1.2", "", {}, "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w=="], + + "mime": ["mime@1.6.0", "", { "bin": { "mime": "cli.js" } }, "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg=="], + + "mime-db": ["mime-db@1.52.0", "", {}, "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg=="], + + "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + + "mkdirp-classic": ["mkdirp-classic@0.5.3", "", {}, "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A=="], + + "morgan": ["morgan@1.10.0", "", { "dependencies": { "basic-auth": "~2.0.1", "debug": "2.6.9", "depd": "~2.0.0", "on-finished": "~2.3.0", "on-headers": "~1.0.2" } }, "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ=="], + + "ms": ["ms@2.0.0", "", {}, "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A=="], + + "nan": ["nan@2.22.2", "", {}, "sha512-DANghxFkS1plDdRsX0X9pm0Z6SJNN6gBdtXfanwoZ8hooC5gosGFSBGRYHUVPz1asKA/kMRqDRdHrluZ61SpBQ=="], + + "negotiator": ["negotiator@0.6.3", "", {}, "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "object-inspect": ["object-inspect@1.13.4", "", {}, "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew=="], + + "on-finished": ["on-finished@2.4.1", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg=="], + + "on-headers": ["on-headers@1.0.2", "", {}, "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "one-time": ["one-time@1.0.0", "", { "dependencies": { "fn.name": "1.x.x" } }, "sha512-5DXOiRKwuSEcQ/l0kGCF6Q3jcADFv5tSmRaJck/OqkVFcOzutB134KRSfF0xDrL39MNnqxbHBbUUcjZIhTgb2g=="], + + "parseurl": ["parseurl@1.3.3", "", {}, "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ=="], + + "path-to-regexp": ["path-to-regexp@0.1.12", "", {}, "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ=="], + + "protobufjs": ["protobufjs@7.5.3", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw=="], + + "proxy-addr": ["proxy-addr@2.0.7", "", { "dependencies": { "forwarded": "0.2.0", "ipaddr.js": "1.9.1" } }, "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "qs": ["qs@6.13.0", "", { "dependencies": { "side-channel": "^1.0.6" } }, "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg=="], + + "range-parser": ["range-parser@1.2.1", "", {}, "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg=="], + + "raw-body": ["raw-body@2.5.2", "", { "dependencies": { "bytes": "3.1.2", "http-errors": "2.0.0", "iconv-lite": "0.4.24", "unpipe": "1.0.0" } }, "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA=="], + + "readable-stream": ["readable-stream@3.6.2", "", { "dependencies": { "inherits": "^2.0.3", "string_decoder": "^1.1.1", "util-deprecate": "^1.0.1" } }, "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="], + + "send": ["send@0.19.0", "", { "dependencies": { "debug": "2.6.9", "depd": "2.0.0", "destroy": "1.2.0", "encodeurl": "~1.0.2", "escape-html": "~1.0.3", "etag": "~1.8.1", "fresh": "0.5.2", "http-errors": "2.0.0", "mime": "1.6.0", "ms": "2.1.3", "on-finished": "2.4.1", "range-parser": "~1.2.1", "statuses": "2.0.1" } }, "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw=="], + + "serve-static": ["serve-static@1.16.2", "", { "dependencies": { "encodeurl": "~2.0.0", "escape-html": "~1.0.3", "parseurl": "~1.3.3", "send": "0.19.0" } }, "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw=="], + + "setprototypeof": ["setprototypeof@1.2.0", "", {}, "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw=="], + + "side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw=="], + + "side-channel-list": ["side-channel-list@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3" } }, "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA=="], + + "side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA=="], + + "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], + + "simple-swizzle": ["simple-swizzle@0.2.2", "", { "dependencies": { "is-arrayish": "^0.3.1" } }, "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg=="], + + "split-ca": ["split-ca@1.0.1", "", {}, "sha512-Q5thBSxp5t8WPTTJQS59LrGqOZqOsrhDGDVm8azCqIBjSBd7nd9o2PM+mDulQQkh8h//4U6hFZnc/mul8t5pWQ=="], + + "ssh2": ["ssh2@1.16.0", "", { "dependencies": { "asn1": "^0.2.6", "bcrypt-pbkdf": "^1.0.2" }, "optionalDependencies": { "cpu-features": "~0.0.10", "nan": "^2.20.0" } }, "sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg=="], + + "stack-trace": ["stack-trace@0.0.10", "", {}, "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg=="], + + "statuses": ["statuses@2.0.1", "", {}, "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "tar-fs": ["tar-fs@2.1.3", "", { "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", "pump": "^3.0.0", "tar-stream": "^2.1.4" } }, "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg=="], + + "tar-stream": ["tar-stream@2.2.0", "", { "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", "fs-constants": "^1.0.0", "inherits": "^2.0.3", "readable-stream": "^3.1.1" } }, "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ=="], + + "text-hex": ["text-hex@1.0.0", "", {}, "sha512-uuVGNWzgJ4yhRaNSiubPY7OjISw4sw4E5Uv0wbjp+OzcbmVU/rsT8ujgcXJhn9ypzsgr5vlzpPqP+MBBKcGvbg=="], + + "toidentifier": ["toidentifier@1.0.1", "", {}, "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA=="], + + "triple-beam": ["triple-beam@1.4.1", "", {}, "sha512-aZbgViZrg1QNcG+LULa7nhZpJTZSLm/mXnHXnbAbjmN5aSa0y7V+wvv6+4WaBtpISJzThKy+PIPxc1Nq1EJ9mg=="], + + "tweetnacl": ["tweetnacl@0.14.5", "", {}, "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="], + + "type-is": ["type-is@1.6.18", "", { "dependencies": { "media-typer": "0.3.0", "mime-types": "~2.1.24" } }, "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g=="], + + "typescript": ["typescript@5.8.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ=="], + + "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + + "unpipe": ["unpipe@1.0.0", "", {}, "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ=="], + + "util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="], + + "utils-merge": ["utils-merge@1.0.1", "", {}, "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA=="], + + "uuid": ["uuid@10.0.0", "", { "bin": { "uuid": "dist/bin/uuid" } }, "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ=="], + + "vary": ["vary@1.1.2", "", {}, "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg=="], + + "winston": ["winston@3.17.0", "", { "dependencies": { "@colors/colors": "^1.6.0", "@dabh/diagnostics": "^2.0.2", "async": "^3.2.3", "is-stream": "^2.0.0", "logform": "^2.7.0", "one-time": "^1.0.0", "readable-stream": "^3.4.0", "safe-stable-stringify": "^2.3.1", "stack-trace": "0.0.x", "triple-beam": "^1.3.0", "winston-transport": "^4.9.0" } }, "sha512-DLiFIXYC5fMPxaRg832S6F5mJYvePtmO5G9v9IgUFPhXm9/GkXarH/TUrBAVzhTCzAj9anE/+GjrgXp/54nOgw=="], + + "winston-transport": ["winston-transport@4.9.0", "", { "dependencies": { "logform": "^2.7.0", "readable-stream": "^3.6.2", "triple-beam": "^1.3.0" } }, "sha512-8drMJ4rkgaPo1Me4zD/3WLfI/zPdA9o2IipKODunnGDcuqbHwjsbB79ylv04LCGGzU0xQ6vTznOMpQGaLhhm6A=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yargs": ["yargs@17.7.2", "", { "dependencies": { "cliui": "^8.0.1", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.3", "y18n": "^5.0.5", "yargs-parser": "^21.1.1" } }, "sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w=="], + + "yargs-parser": ["yargs-parser@21.1.1", "", {}, "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw=="], + + "@types/body-parser/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "@types/connect/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "@types/cors/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "@types/express-serve-static-core/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "@types/morgan/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "@types/send/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "@types/serve-static/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "ansi-styles/color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "basic-auth/safe-buffer": ["safe-buffer@5.1.2", "", {}, "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="], + + "bun-types/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "color-string/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "docker-modem/debug": ["debug@4.4.1", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ=="], + + "logform/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "morgan/on-finished": ["on-finished@2.3.0", "", { "dependencies": { "ee-first": "1.1.1" } }, "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww=="], + + "protobufjs/@types/node": ["@types/node@24.0.10", "", { "dependencies": { "undici-types": "~7.8.0" } }, "sha512-ENHwaH+JIRTDIEEbDK6QSQntAYGtbvdDXnMXnZaZ6k13Du1dPMmprkEHIL7ok2Wl2aZevetwTAb5S+7yIF+enA=="], + + "send/encodeurl": ["encodeurl@1.0.2", "", {}, "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w=="], + + "send/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "@types/body-parser/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "@types/connect/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "@types/cors/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "@types/express-serve-static-core/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "@types/morgan/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "@types/send/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "@types/serve-static/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "ansi-styles/color-convert/color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "bun-types/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + + "docker-modem/debug/ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "protobufjs/@types/node/undici-types": ["undici-types@7.8.0", "", {}, "sha512-9UJ2xGDvQ43tYyVMpuHlsgApydB8ZKfVYTsLDhXkFL/6gfkp+U8xTGdh8pMJv1SpZna0zxG1DwsKZsreLbXBxw=="], + } +} diff --git a/node/docker-compose.dev.yml b/node/docker-compose.dev.yml new file mode 100644 index 0000000..1057009 --- /dev/null +++ b/node/docker-compose.dev.yml @@ -0,0 +1,49 @@ +version: '3.8' + +services: + # Home Server Agent (Development) + home-server-agent: + build: . + container_name: home-server-agent-dev + ports: + - "3000:3000" + environment: + - NODE_ENV=development + - API_TOKEN=dev-token-123 + - LOG_LEVEL=debug + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./logs:/app/logs + - ./src:/app/src + networks: + - game-network + restart: unless-stopped + + # Single Minecraft Server for Testing + minecraft: + image: itzg/minecraft-server:latest + container_name: gameserver-minecraft + ports: + - "25565:25565" + environment: + EULA: "TRUE" + TYPE: "VANILLA" + MEMORY: "1G" + DIFFICULTY: "peaceful" + MAX_PLAYERS: "5" + ONLINE_MODE: "false" + volumes: + - minecraft-dev-data:/data + networks: + - game-network + restart: unless-stopped + labels: + - "game-server=minecraft" + +volumes: + minecraft-dev-data: + driver: local + +networks: + game-network: + driver: bridge diff --git a/node/docker-compose.yml b/node/docker-compose.yml new file mode 100644 index 0000000..62a81c2 --- /dev/null +++ b/node/docker-compose.yml @@ -0,0 +1,153 @@ +version: '3.8' + +services: + # Home Server Agent + home-server-agent: + build: . + container_name: home-server-agent + ports: + - "3000:3000" + environment: + - NODE_ENV=production + - API_TOKEN=${API_TOKEN:-your-secret-token-here} + - LOG_LEVEL=info + - FRPC_CONFIG_PATH=${FRPC_CONFIG_PATH:-/app/data/frpc.toml} + - FRPC_CONTAINER_NAME=${FRPC_CONTAINER_NAME:-frpc} + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - ./logs:/app/logs + - ./data:/app/data + networks: + - game-network + restart: unless-stopped + depends_on: + - minecraft + - valheim + - terraria + + # Minecraft Server + minecraft: + image: itzg/minecraft-server:latest + container_name: gameserver-minecraft + ports: + - "25565:25565" + environment: + EULA: "TRUE" + TYPE: "VANILLA" + MEMORY: "2G" + DIFFICULTY: "normal" + SPAWN_PROTECTION: "0" + MAX_PLAYERS: "20" + ONLINE_MODE: "false" + ALLOW_NETHER: "true" + ANNOUNCE_PLAYER_ACHIEVEMENTS: "true" + ENABLE_COMMAND_BLOCK: "true" + FORCE_GAMEMODE: "false" + GENERATE_STRUCTURES: "true" + HARDCORE: "false" + MAX_BUILD_HEIGHT: "256" + MAX_TICK_TIME: "60000" + SPAWN_ANIMALS: "true" + SPAWN_MONSTERS: "true" + SPAWN_NPCS: "true" + VIEW_DISTANCE: "10" + volumes: + - minecraft-data:/data + networks: + - game-network + restart: unless-stopped + labels: + - "game-server=minecraft" + + # Valheim Server + valheim: + image: lloesche/valheim-server:latest + container_name: gameserver-valheim + ports: + - "2456:2456/udp" + - "2457:2457/udp" + environment: + SERVER_NAME: "My Valheim Server" + WORLD_NAME: "MyWorld" + SERVER_PASS: "secret123" + SERVER_PUBLIC: "false" + ADMINLIST_IDS: "" + BANNEDLIST_IDS: "" + PERMITTEDLIST_IDS: "" + volumes: + - valheim-data:/config + networks: + - game-network + restart: unless-stopped + labels: + - "game-server=valheim" + + # Terraria Server + terraria: + image: ryshe/terraria:latest + container_name: gameserver-terraria + ports: + - "7777:7777" + environment: + WORLD: "MyWorld" + PASSWORD: "secret123" + MAXPLAYERS: "16" + DIFFICULTY: "1" + AUTOCREATE: "2" + BANLIST: "" + SECURE: "1" + LANGUAGE: "en-US" + volumes: + - terraria-data:/world + networks: + - game-network + restart: unless-stopped + labels: + - "game-server=terraria" + + # Optional: Portainer for Docker management + portainer: + image: portainer/portainer-ce:latest + container_name: portainer + ports: + - "9000:9000" + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + - portainer-data:/data + networks: + - game-network + restart: unless-stopped + profiles: + - management + + # Optional: Watchtower for automatic updates + watchtower: + image: containrrr/watchtower:latest + container_name: watchtower + volumes: + - /var/run/docker.sock:/var/run/docker.sock:ro + environment: + - WATCHTOWER_CLEANUP=true + - WATCHTOWER_SCHEDULE=0 0 2 * * * # 2 AM daily + networks: + - game-network + restart: unless-stopped + profiles: + - management + +volumes: + minecraft-data: + driver: local + valheim-data: + driver: local + terraria-data: + driver: local + portainer-data: + driver: local + +networks: + game-network: + driver: bridge + ipam: + config: + - subnet: 172.20.0.0/16 diff --git a/node/health-check.ps1 b/node/health-check.ps1 new file mode 100644 index 0000000..cf40ce3 --- /dev/null +++ b/node/health-check.ps1 @@ -0,0 +1,110 @@ +# Health check script for the Home Server Agent (PowerShell) +# This script can be used with monitoring tools on Windows + +param( + [Parameter(Mandatory=$true)] + [ValidateSet('health', 'gameserver')] + [string]$CheckType, + + [Parameter(Mandatory=$false)] + [string]$ServerName, + + [Parameter(Mandatory=$false)] + [string]$AgentUrl = "http://localhost:3000", + + [Parameter(Mandatory=$false)] + [string]$ApiToken = $env:API_TOKEN, + + [Parameter(Mandatory=$false)] + [int]$Timeout = 10 +) + +# Function to check if agent is healthy +function Test-AgentHealth { + try { + $headers = @{} + if ($ApiToken) { + $headers["Authorization"] = "Bearer $ApiToken" + } + + $response = Invoke-RestMethod -Uri "$AgentUrl/health" -Headers $headers -TimeoutSec $Timeout -ErrorAction Stop + + if ($response.status -eq "healthy") { + Write-Output "OK: Home Server Agent is healthy" + return 0 + } else { + Write-Output "CRITICAL: Home Server Agent reported unhealthy status" + return 2 + } + } + catch { + Write-Output "CRITICAL: Home Server Agent is not responding - $($_.Exception.Message)" + return 2 + } +} + +# Function to check game server status +function Test-GameServerStatus { + param([string]$ServerName) + + if (-not $ServerName) { + Write-Output "UNKNOWN: Server name not provided" + return 3 + } + + if (-not $ApiToken) { + Write-Output "UNKNOWN: API_TOKEN not provided" + return 3 + } + + try { + $headers = @{ + "Authorization" = "Bearer $ApiToken" + } + + $response = Invoke-RestMethod -Uri "$AgentUrl/api/gameserver/$ServerName/status" -Headers $headers -TimeoutSec $Timeout -ErrorAction Stop + + switch ($response.status) { + "running" { + Write-Output "OK: $ServerName is running" + return 0 + } + "stopped" { + Write-Output "WARNING: $ServerName is stopped" + return 1 + } + default { + Write-Output "CRITICAL: $ServerName status unknown ($($response.status))" + return 2 + } + } + } + catch { + Write-Output "CRITICAL: Cannot check $ServerName status - $($_.Exception.Message)" + return 2 + } +} + +# Main execution +$exitCode = 0 + +switch ($CheckType) { + "health" { + $exitCode = Test-AgentHealth + } + "gameserver" { + $exitCode = Test-GameServerStatus -ServerName $ServerName + } + default { + Write-Output "Usage: health-check.ps1 -CheckType {health|gameserver} [-ServerName ]" + Write-Output "Environment variables:" + Write-Output " API_TOKEN - Authentication token for API access" + Write-Output "Parameters:" + Write-Output " -AgentUrl - URL of the Home Server Agent (default: http://localhost:3000)" + Write-Output " -ApiToken - Authentication token for API access" + Write-Output " -Timeout - Request timeout in seconds (default: 10)" + $exitCode = 3 + } +} + +exit $exitCode diff --git a/node/health-check.sh b/node/health-check.sh new file mode 100644 index 0000000..1b1edc0 --- /dev/null +++ b/node/health-check.sh @@ -0,0 +1,98 @@ +#!/bin/bash + +# Health check script for the Home Server Agent +# This script can be used with monitoring tools like Nagios, Zabbix, etc. + +AGENT_URL="${AGENT_URL:-http://localhost:3000}" +API_TOKEN="${API_TOKEN:-}" +TIMEOUT="${TIMEOUT:-10}" + +# Function to check if agent is healthy +check_health() { + local response + local status_code + + if [ -n "$API_TOKEN" ]; then + response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \ + -H "Authorization: Bearer $API_TOKEN" \ + "$AGENT_URL/health" 2>/dev/null) + else + response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \ + "$AGENT_URL/health" 2>/dev/null) + fi + + status_code="${response: -3}" + + if [ "$status_code" = "200" ]; then + echo "OK: Home Server Agent is healthy" + return 0 + else + echo "CRITICAL: Home Server Agent is not responding (HTTP $status_code)" + return 2 + fi +} + +# Function to check game server status +check_gameserver() { + local server_name="$1" + local response + local status_code + + if [ -z "$server_name" ]; then + echo "UNKNOWN: Server name not provided" + return 3 + fi + + if [ -z "$API_TOKEN" ]; then + echo "UNKNOWN: API_TOKEN not provided" + return 3 + fi + + response=$(curl -s -w "%{http_code}" -m "$TIMEOUT" \ + -H "Authorization: Bearer $API_TOKEN" \ + "$AGENT_URL/api/gameserver/$server_name/status" 2>/dev/null) + + status_code="${response: -3}" + response_body="${response%???}" + + if [ "$status_code" = "200" ]; then + # Parse JSON response to get status + status=$(echo "$response_body" | grep -o '"status":"[^"]*"' | cut -d'"' -f4) + + case "$status" in + "running") + echo "OK: $server_name is running" + return 0 + ;; + "stopped") + echo "WARNING: $server_name is stopped" + return 1 + ;; + *) + echo "CRITICAL: $server_name status unknown ($status)" + return 2 + ;; + esac + else + echo "CRITICAL: Cannot check $server_name status (HTTP $status_code)" + return 2 + fi +} + +# Main execution +case "$1" in + "health") + check_health + ;; + "gameserver") + check_gameserver "$2" + ;; + *) + echo "Usage: $0 {health|gameserver }" + echo "Environment variables:" + echo " AGENT_URL - URL of the Home Server Agent (default: http://localhost:3000)" + echo " API_TOKEN - Authentication token for API access" + echo " TIMEOUT - Request timeout in seconds (default: 10)" + exit 3 + ;; +esac diff --git a/node/home-server-agent.service b/node/home-server-agent.service new file mode 100644 index 0000000..ead21c5 --- /dev/null +++ b/node/home-server-agent.service @@ -0,0 +1,23 @@ +[Unit] +Description=Home Server Agent +Documentation=https://github.com/your-repo/home-server-agent +Requires=docker.service +After=docker.service +Wants=network-online.target +After=network-online.target + +[Service] +Type=forking +RemainAfterExit=yes +WorkingDirectory=/opt/home-server-agent +ExecStart=/usr/local/bin/docker-compose up -d +ExecStop=/usr/local/bin/docker-compose down +ExecReload=/usr/local/bin/docker-compose restart +TimeoutStartSec=0 +Restart=on-failure +RestartSec=10 +User=docker +Group=docker + +[Install] +WantedBy=multi-user.target diff --git a/node/install-windows-service.ps1 b/node/install-windows-service.ps1 new file mode 100644 index 0000000..40cc80a --- /dev/null +++ b/node/install-windows-service.ps1 @@ -0,0 +1,54 @@ +# Windows Service Installation Script +# Run as Administrator + +$serviceName = "HomeServerAgent" +$serviceDisplayName = "Home Server Agent" +$serviceDescription = "Lightweight agent for managing game servers" +$servicePath = "C:\Program Files\HomeServerAgent" +$dockerComposePath = "$servicePath\docker-compose.yml" + +# Check if running as administrator +if (-NOT ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) { + Write-Warning "Please run this script as Administrator" + exit 1 +} + +# Create service directory +if (!(Test-Path $servicePath)) { + New-Item -ItemType Directory -Path $servicePath -Force +} + +# Copy files to service directory +Copy-Item -Path ".\*" -Destination $servicePath -Recurse -Force + +# Install NSSM (Non-Sucking Service Manager) if not present +$nssmPath = "$servicePath\nssm.exe" +if (!(Test-Path $nssmPath)) { + Write-Host "Downloading NSSM..." + Invoke-WebRequest -Uri "https://nssm.cc/release/nssm-2.24.zip" -OutFile "$servicePath\nssm.zip" + Expand-Archive -Path "$servicePath\nssm.zip" -DestinationPath $servicePath + Copy-Item -Path "$servicePath\nssm-2.24\win64\nssm.exe" -Destination $nssmPath + Remove-Item -Path "$servicePath\nssm.zip" -Force + Remove-Item -Path "$servicePath\nssm-2.24" -Recurse -Force +} + +# Create service +& $nssmPath install $serviceName "docker-compose" +& $nssmPath set $serviceName AppDirectory $servicePath +& $nssmPath set $serviceName AppParameters "up -d" +& $nssmPath set $serviceName DisplayName $serviceDisplayName +& $nssmPath set $serviceName Description $serviceDescription +& $nssmPath set $serviceName Start SERVICE_AUTO_START +& $nssmPath set $serviceName AppStopMethodConsole 30000 +& $nssmPath set $serviceName AppStopMethodWindow 30000 +& $nssmPath set $serviceName AppStopMethodThreads 30000 +& $nssmPath set $serviceName AppKillProcessTree 1 + +# Set service to restart on failure +& $nssmPath set $serviceName AppRestartDelay 10000 +& $nssmPath set $serviceName AppNoConsole 1 + +Write-Host "Service installed successfully!" +Write-Host "To start the service: Start-Service $serviceName" +Write-Host "To stop the service: Stop-Service $serviceName" +Write-Host "To remove the service: & '$nssmPath' remove $serviceName confirm" diff --git a/node/package.json b/node/package.json new file mode 100644 index 0000000..39f6f53 --- /dev/null +++ b/node/package.json @@ -0,0 +1,34 @@ +{ + "name": "home-server-agent", + "version": "1.0.0", + "description": "Lightweight agent for managing game servers on home server", + "main": "dist/index.js", + "scripts": { + "dev": "bun run --watch src/index.ts", + "build": "bun build src/index.ts --outdir dist --target node", + "start": "node dist/index.js", + "docker:build": "docker build -t home-server-agent .", + "docker:run": "docker-compose up -d" + }, + "type": "module", + "private": true, + "dependencies": { + "express": "^4.18.2", + "cors": "^2.8.5", + "helmet": "^7.1.0", + "morgan": "^1.10.0", + "winston": "^3.11.0", + "dockerode": "^4.0.2", + "dotenv": "^16.3.1" + }, + "devDependencies": { + "@types/bun": "latest", + "@types/express": "^4.17.21", + "@types/cors": "^2.8.17", + "@types/morgan": "^1.9.9", + "@types/node": "^20.10.0" + }, + "peerDependencies": { + "typescript": "^5" + } +} \ No newline at end of file diff --git a/node/src/index.ts b/node/src/index.ts new file mode 100644 index 0000000..d7e527a --- /dev/null +++ b/node/src/index.ts @@ -0,0 +1,83 @@ +import express from 'express'; +import cors from 'cors'; +import helmet from 'helmet'; +import morgan from 'morgan'; +import dotenv from 'dotenv'; +import { logger } from './utils/logger.js'; +import { authMiddleware } from './middleware/auth.js'; +import { statusRouter } from './routes/status.js'; +import { gameServerRouter } from './routes/gameServer.js'; +import { frpcRouter } from './routes/frpc.js'; +import { DockerManager } from './services/dockerManager.js'; + +// Load environment variables +dotenv.config(); + +const app = express(); +const PORT = process.env.PORT || 3000; + +// Initialize Docker manager +const dockerManager = new DockerManager(); + +// Middleware +app.use(helmet()); +app.use(cors()); +app.use(morgan('combined', { stream: { write: (message) => logger.info(message.trim()) } })); +app.use(express.json()); + +// Authentication middleware for protected routes +app.use('/api', authMiddleware); + +// Routes +app.use('/api/status', statusRouter); +app.use('/api/gameserver', gameServerRouter); +app.use('/api/frpc', frpcRouter); + +// Health check endpoint (no auth required) +app.get('/health', (req, res) => { + res.json({ status: 'healthy', timestamp: new Date().toISOString() }); +}); + +// Root endpoint +app.get('/', (req, res) => { + res.json({ + name: 'Home Server Agent', + version: '1.0.0', + description: 'Lightweight agent for managing game servers', + endpoints: { + '/health': 'Health check (no auth)', + '/api/status': 'Get server status', + '/api/gameserver/list': 'List game servers', + '/api/gameserver/start/:serviceName': 'Start a game server', + '/api/gameserver/stop/:serviceName': 'Stop a game server', + '/api/gameserver/:serviceName/status': 'Get specific server status', + '/api/frpc/status': 'Get frpc status', + '/api/frpc/update-config': 'Update frpc configuration', + '/api/frpc/restart': 'Restart frpc container', + '/api/frpc/push-and-restart': 'Update config and restart frpc' + } + }); +}); + +// Error handling middleware +app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { + logger.error(`Error: ${err.message}`, { stack: err.stack }); + res.status(500).json({ error: 'Internal server error' }); +}); + +// Start server +app.listen(PORT, () => { + logger.info(`Home Server Agent listening on port ${PORT}`); + logger.info(`Environment: ${process.env.NODE_ENV || 'development'}`); +}); + +// Graceful shutdown +process.on('SIGTERM', () => { + logger.info('Received SIGTERM, shutting down gracefully'); + process.exit(0); +}); + +process.on('SIGINT', () => { + logger.info('Received SIGINT, shutting down gracefully'); + process.exit(0); +}); diff --git a/node/src/middleware/auth.ts b/node/src/middleware/auth.ts new file mode 100644 index 0000000..b5dadd0 --- /dev/null +++ b/node/src/middleware/auth.ts @@ -0,0 +1,25 @@ +import { Request, Response, NextFunction } from 'express'; +import { logger } from '../utils/logger.js'; + +export const authMiddleware = (req: Request, res: Response, next: NextFunction) => { + const token = req.headers.authorization?.replace('Bearer ', '') || req.headers['x-api-key']; + const expectedToken = process.env.API_TOKEN; + + if (!expectedToken) { + logger.warn('No API_TOKEN configured, skipping authentication'); + return next(); + } + + if (!token) { + logger.warn(`Unauthorized request from ${req.ip} - no token provided`); + return res.status(401).json({ error: 'Authentication required' }); + } + + if (token !== expectedToken) { + logger.warn(`Unauthorized request from ${req.ip} - invalid token`); + return res.status(403).json({ error: 'Invalid token' }); + } + + logger.info(`Authenticated request from ${req.ip}`); + next(); +}; diff --git a/node/src/routes/frpc.ts b/node/src/routes/frpc.ts new file mode 100644 index 0000000..68d5983 --- /dev/null +++ b/node/src/routes/frpc.ts @@ -0,0 +1,158 @@ +import { Router, Request, Response } from 'express'; +import { logger } from '../utils/logger.js'; +import fs from 'fs/promises'; +import path from 'path'; +import { exec } from 'child_process'; +import { promisify } from 'util'; + +const execAsync = promisify(exec); +const router = Router(); + +// Path to frpc config file +const FRPC_CONFIG_PATH = process.env.FRPC_CONFIG_PATH || '/app/data/frpc.toml'; +const FRPC_CONTAINER_NAME = process.env.FRPC_CONTAINER_NAME || 'frpc'; + +// Update frpc configuration +router.post('/update-config', async (req: Request, res: Response) => { + try { + const { config } = req.body; + + if (!config || typeof config !== 'string') { + res.status(400).json({ + success: false, + message: 'Invalid config format' + }); + return; + } + + // Ensure directory exists + const dir = path.dirname(FRPC_CONFIG_PATH); + await fs.mkdir(dir, { recursive: true }); + + // Write config to file + await fs.writeFile(FRPC_CONFIG_PATH, config); + + logger.info('frpc configuration updated successfully'); + res.json({ + success: true, + message: 'Configuration updated successfully' + }); + } catch (error) { + logger.error('Failed to update frpc configuration:', error); + res.status(500).json({ + success: false, + message: 'Failed to update configuration' + }); + } +}); + +// Restart frpc container +router.post('/restart', async (req: Request, res: Response) => { + try { + // Check if container exists + const { stdout: containers } = await execAsync(`docker ps -a --filter "name=${FRPC_CONTAINER_NAME}" --format "{{.Names}}"`); + + if (!containers.includes(FRPC_CONTAINER_NAME)) { + logger.warn(`frpc container ${FRPC_CONTAINER_NAME} not found`); + res.status(404).json({ + success: false, + message: 'frpc container not found' + }); + return; + } + + // Restart the container + await execAsync(`docker restart ${FRPC_CONTAINER_NAME}`); + + logger.info(`frpc container ${FRPC_CONTAINER_NAME} restarted successfully`); + res.json({ + success: true, + message: 'frpc restarted successfully' + }); + } catch (error) { + logger.error('Failed to restart frpc container:', error); + res.status(500).json({ + success: false, + message: 'Failed to restart frpc' + }); + } +}); + +// Get frpc status +router.get('/status', async (req: Request, res: Response) => { + try { + const { stdout } = await execAsync(`docker ps --filter "name=${FRPC_CONTAINER_NAME}" --format "{{.Names}} {{.Status}}"`); + + const isRunning = stdout.trim().includes(FRPC_CONTAINER_NAME); + + res.json({ + container: FRPC_CONTAINER_NAME, + running: isRunning, + status: isRunning ? 'running' : 'stopped', + timestamp: new Date().toISOString() + }); + } catch (error) { + logger.error('Failed to get frpc status:', error); + res.status(500).json({ + running: false, + status: 'error', + message: 'Failed to get frpc status' + }); + } +}); + +// Get frpc logs +router.get('/logs', async (req: Request, res: Response) => { + try { + const lines = parseInt(req.query.lines as string) || 50; + const { stdout } = await execAsync(`docker logs --tail ${lines} ${FRPC_CONTAINER_NAME}`); + + res.json({ + logs: stdout, + timestamp: new Date().toISOString() + }); + } catch (error) { + logger.error('Failed to get frpc logs:', error); + res.status(500).json({ + logs: '', + message: 'Failed to get frpc logs' + }); + } +}); + +// Push and restart - convenience endpoint +router.post('/push-and-restart', async (req: Request, res: Response) => { + try { + const { config } = req.body; + + if (!config || typeof config !== 'string') { + res.status(400).json({ + success: false, + message: 'Invalid config format' + }); + return; + } + + // Update config + const dir = path.dirname(FRPC_CONFIG_PATH); + await fs.mkdir(dir, { recursive: true }); + await fs.writeFile(FRPC_CONFIG_PATH, config); + + // Restart frpc + await execAsync(`docker restart ${FRPC_CONTAINER_NAME}`); + + logger.info('frpc configuration updated and container restarted'); + res.json({ + success: true, + message: 'Configuration updated and frpc restarted successfully' + }); + } catch (error) { + logger.error('Failed to push config and restart frpc:', error); + res.status(500).json({ + success: false, + message: 'Failed to update configuration and restart frpc' + }); + } +}); + +export { router as frpcRouter }; diff --git a/node/src/routes/gameServer.ts b/node/src/routes/gameServer.ts new file mode 100644 index 0000000..180d407 --- /dev/null +++ b/node/src/routes/gameServer.ts @@ -0,0 +1,138 @@ +import { Router, Request, Response } from 'express'; +import { DockerManager } from '../services/dockerManager.js'; +import { logger } from '../utils/logger.js'; + +const router = Router(); +const dockerManager = new DockerManager(); + +// List all available game servers +router.get('/list', async (req: Request, res: Response) => { + try { + const servers = await dockerManager.listGameServers(); + res.json(servers); + } catch (error) { + logger.error('Error listing game servers:', error); + res.status(500).json({ error: 'Failed to list game servers' }); + } +}); + +// Start a game server +router.post('/start/:serviceName', async (req: Request, res: Response) => { + try { + const { serviceName } = req.params; + logger.info(`Request to start game server: ${serviceName}`); + + const result = await dockerManager.startServer(serviceName); + + if (result.success) { + res.json({ + success: true, + message: result.message, + serviceName + }); + } else { + res.status(400).json({ + success: false, + message: result.message, + serviceName + }); + } + } catch (error) { + logger.error(`Error starting game server ${req.params.serviceName}:`, error); + res.status(500).json({ + success: false, + message: 'Internal server error', + serviceName: req.params.serviceName + }); + } +}); + +// Stop a game server +router.post('/stop/:serviceName', async (req: Request, res: Response) => { + try { + const { serviceName } = req.params; + logger.info(`Request to stop game server: ${serviceName}`); + + const result = await dockerManager.stopServer(serviceName); + + if (result.success) { + res.json({ + success: true, + message: result.message, + serviceName + }); + } else { + res.status(400).json({ + success: false, + message: result.message, + serviceName + }); + } + } catch (error) { + logger.error(`Error stopping game server ${req.params.serviceName}:`, error); + res.status(500).json({ + success: false, + message: 'Internal server error', + serviceName: req.params.serviceName + }); + } +}); + +// Get status of a specific game server +router.get('/:serviceName/status', async (req: Request, res: Response) => { + try { + const { serviceName } = req.params; + const status = await dockerManager.getServerStatus(serviceName); + res.json(status); + } catch (error) { + logger.error(`Error getting status for ${req.params.serviceName}:`, error); + res.status(500).json({ error: 'Failed to get server status' }); + } +}); + +// Restart a game server +router.post('/restart/:serviceName', async (req: Request, res: Response) => { + try { + const { serviceName } = req.params; + logger.info(`Request to restart game server: ${serviceName}`); + + // Stop first + const stopResult = await dockerManager.stopServer(serviceName); + if (!stopResult.success && !stopResult.message.includes('not running')) { + return res.status(400).json({ + success: false, + message: `Failed to stop ${serviceName}: ${stopResult.message}`, + serviceName + }); + } + + // Wait a moment + await new Promise(resolve => setTimeout(resolve, 2000)); + + // Start again + const startResult = await dockerManager.startServer(serviceName); + + if (startResult.success) { + res.json({ + success: true, + message: `${serviceName} restarted successfully`, + serviceName + }); + } else { + res.status(400).json({ + success: false, + message: `Failed to restart ${serviceName}: ${startResult.message}`, + serviceName + }); + } + } catch (error) { + logger.error(`Error restarting game server ${req.params.serviceName}:`, error); + res.status(500).json({ + success: false, + message: 'Internal server error', + serviceName: req.params.serviceName + }); + } +}); + +export { router as gameServerRouter }; diff --git a/node/src/routes/status.ts b/node/src/routes/status.ts new file mode 100644 index 0000000..d9c2b9c --- /dev/null +++ b/node/src/routes/status.ts @@ -0,0 +1,78 @@ +import { Router, Request, Response } from 'express'; +import { DockerManager } from '../services/dockerManager.js'; +import { logger } from '../utils/logger.js'; + +const router = Router(); +const dockerManager = new DockerManager(); + +// Get overall server status +router.get('/', async (req: Request, res: Response) => { + try { + const [gameServers, systemStats, runningContainers] = await Promise.all([ + dockerManager.listGameServers(), + dockerManager.getSystemStats(), + dockerManager.getRunningContainers() + ]); + + const activePorts = new Set(); + runningContainers.forEach((container: any) => { + if (container.Ports) { + container.Ports.forEach((port: any) => { + if (port.PublicPort) { + activePorts.add(port.PublicPort); + } + }); + } + }); + + res.json({ + timestamp: new Date().toISOString(), + status: 'operational', + gameServers: { + available: gameServers.available, + running: gameServers.running.length, + runningServers: gameServers.running + }, + activePorts: Array.from(activePorts).sort(), + systemStats, + containers: { + total: runningContainers.length, + running: runningContainers.filter((c: any) => c.State === 'running').length + } + }); + } catch (error) { + logger.error('Error getting server status:', error); + res.status(500).json({ error: 'Failed to get server status' }); + } +}); + +// Get active ports and services +router.get('/ports', async (req: Request, res: Response) => { + try { + const containers = await dockerManager.getRunningContainers(); + const portMappings: { [key: number]: string } = {}; + + containers.forEach((container: any) => { + if (container.Ports && container.State === 'running') { + container.Ports.forEach((port: any) => { + if (port.PublicPort) { + const serviceName = container.Labels?.['game-server'] || + container.Names[0]?.replace('/', '') || + 'unknown'; + portMappings[port.PublicPort] = serviceName; + } + }); + } + }); + + res.json({ + activePorts: portMappings, + totalPorts: Object.keys(portMappings).length + }); + } catch (error) { + logger.error('Error getting port information:', error); + res.status(500).json({ error: 'Failed to get port information' }); + } +}); + +export { router as statusRouter }; diff --git a/node/src/services/dockerManager.ts b/node/src/services/dockerManager.ts new file mode 100644 index 0000000..0abc992 --- /dev/null +++ b/node/src/services/dockerManager.ts @@ -0,0 +1,247 @@ +import Docker from 'dockerode'; +import { logger } from '../utils/logger.js'; + +export interface GameServerConfig { + name: string; + image: string; + ports: { [key: string]: number }; + environment?: { [key: string]: string }; + volumes?: string[]; + restart?: string; +} + +export interface ServerStatus { + name: string; + status: 'running' | 'stopped' | 'not-found'; + containerId?: string; + ports?: { [key: string]: number }; + uptime?: string; +} + +export class DockerManager { + private docker: Docker; + private gameServers: Map; + + constructor() { + this.docker = new Docker(); + this.gameServers = new Map(); + this.initializeGameServers(); + } + + private initializeGameServers() { + // Define available game servers + const servers: GameServerConfig[] = [ + { + name: 'minecraft', + image: 'itzg/minecraft-server:latest', + ports: { '25565': 25565 }, + environment: { + EULA: 'TRUE', + TYPE: 'VANILLA', + MEMORY: '2G' + }, + volumes: ['/data/minecraft:/data'], + restart: 'unless-stopped' + }, + { + name: 'valheim', + image: 'lloesche/valheim-server:latest', + ports: { '2456': 2456, '2457': 2457 }, + environment: { + SERVER_NAME: 'My Valheim Server', + WORLD_NAME: 'MyWorld', + SERVER_PASS: 'secret123' + }, + volumes: ['/data/valheim:/config'], + restart: 'unless-stopped' + }, + { + name: 'terraria', + image: 'ryshe/terraria:latest', + ports: { '7777': 7777 }, + environment: { + WORLD: 'MyWorld', + PASSWORD: 'secret123' + }, + volumes: ['/data/terraria:/world'], + restart: 'unless-stopped' + } + ]; + + servers.forEach(server => { + this.gameServers.set(server.name, server); + }); + } + + async getRunningContainers(): Promise { + try { + const containers = await this.docker.listContainers(); + return containers; + } catch (error) { + logger.error('Error listing containers:', error); + return []; + } + } + + async getServerStatus(serviceName: string): Promise { + try { + const containers = await this.docker.listContainers({ all: true }); + const container = containers.find(c => + c.Names.some(name => name.includes(serviceName)) || + c.Labels?.['game-server'] === serviceName + ); + + if (!container) { + return { + name: serviceName, + status: 'not-found' + }; + } + + const status = container.State === 'running' ? 'running' : 'stopped'; + const ports: { [key: string]: number } = {}; + + if (container.Ports) { + container.Ports.forEach(port => { + if (port.PublicPort) { + ports[port.PrivatePort.toString()] = port.PublicPort; + } + }); + } + + return { + name: serviceName, + status, + containerId: container.Id, + ports, + uptime: status === 'running' ? this.calculateUptime(container.Created) : undefined + }; + } catch (error) { + logger.error(`Error getting status for ${serviceName}:`, error); + return { + name: serviceName, + status: 'not-found' + }; + } + } + + async startServer(serviceName: string): Promise<{ success: boolean; message: string }> { + try { + const config = this.gameServers.get(serviceName); + if (!config) { + return { success: false, message: `Unknown game server: ${serviceName}` }; + } + + // Check if container already exists + const status = await this.getServerStatus(serviceName); + if (status.status === 'running') { + return { success: false, message: `${serviceName} is already running` }; + } + + if (status.containerId) { + // Container exists but is stopped, start it + const container = this.docker.getContainer(status.containerId); + await container.start(); + logger.info(`Started existing container for ${serviceName}`); + } else { + // Create new container + const containerOptions = { + Image: config.image, + name: `gameserver-${serviceName}`, + Labels: { + 'game-server': serviceName + }, + Env: config.environment ? Object.entries(config.environment).map(([k, v]) => `${k}=${v}`) : [], + ExposedPorts: Object.keys(config.ports).reduce((acc, port) => { + acc[`${port}/tcp`] = {}; + return acc; + }, {} as any), + HostConfig: { + PortBindings: Object.entries(config.ports).reduce((acc, [privatePort, publicPort]) => { + acc[`${privatePort}/tcp`] = [{ HostPort: publicPort.toString() }]; + return acc; + }, {} as any), + Binds: config.volumes || [], + RestartPolicy: { + Name: config.restart || 'unless-stopped' + } + } + }; + + const container = await this.docker.createContainer(containerOptions); + await container.start(); + logger.info(`Created and started new container for ${serviceName}`); + } + + return { success: true, message: `${serviceName} started successfully` }; + } catch (error) { + logger.error(`Error starting ${serviceName}:`, error); + return { success: false, message: `Failed to start ${serviceName}: ${error}` }; + } + } + + async stopServer(serviceName: string): Promise<{ success: boolean; message: string }> { + try { + const status = await this.getServerStatus(serviceName); + if (status.status !== 'running') { + return { success: false, message: `${serviceName} is not running` }; + } + + if (status.containerId) { + const container = this.docker.getContainer(status.containerId); + await container.stop(); + logger.info(`Stopped container for ${serviceName}`); + return { success: true, message: `${serviceName} stopped successfully` }; + } + + return { success: false, message: `Could not find container for ${serviceName}` }; + } catch (error) { + logger.error(`Error stopping ${serviceName}:`, error); + return { success: false, message: `Failed to stop ${serviceName}: ${error}` }; + } + } + + async listGameServers(): Promise<{ available: string[]; running: ServerStatus[] }> { + const available = Array.from(this.gameServers.keys()); + const running: ServerStatus[] = []; + + for (const serverName of available) { + const status = await this.getServerStatus(serverName); + if (status.status === 'running') { + running.push(status); + } + } + + return { available, running }; + } + + async getSystemStats(): Promise { + try { + const info = await this.docker.info(); + const version = await this.docker.version(); + return { + dockerVersion: version.Version, + containers: info.Containers, + containersRunning: info.ContainersRunning, + containersPaused: info.ContainersPaused, + containersStopped: info.ContainersStopped, + images: info.Images, + memoryLimit: info.MemoryLimit, + swapLimit: info.SwapLimit, + cpus: info.NCPU, + architecture: info.Architecture + }; + } catch (error) { + logger.error('Error getting system stats:', error); + return null; + } + } + + private calculateUptime(created: number): string { + const now = Date.now(); + const uptime = now - (created * 1000); + const hours = Math.floor(uptime / (1000 * 60 * 60)); + const minutes = Math.floor((uptime % (1000 * 60 * 60)) / (1000 * 60)); + return `${hours}h ${minutes}m`; + } +} diff --git a/node/src/utils/logger.ts b/node/src/utils/logger.ts new file mode 100644 index 0000000..c3cb908 --- /dev/null +++ b/node/src/utils/logger.ts @@ -0,0 +1,26 @@ +import winston from 'winston'; + +const { combine, timestamp, errors, json, simple, colorize } = winston.format; + +export const logger = winston.createLogger({ + level: process.env.LOG_LEVEL || 'info', + format: combine( + timestamp(), + errors({ stack: true }), + json() + ), + transports: [ + new winston.transports.File({ filename: 'logs/error.log', level: 'error' }), + new winston.transports.File({ filename: 'logs/combined.log' }) + ] +}); + +// If not in production, log to console as well +if (process.env.NODE_ENV !== 'production') { + logger.add(new winston.transports.Console({ + format: combine( + colorize(), + simple() + ) + })); +} diff --git a/node/test-api.js b/node/test-api.js new file mode 100644 index 0000000..58bc5e5 --- /dev/null +++ b/node/test-api.js @@ -0,0 +1,67 @@ +#!/usr/bin/env node + +// Simple test script to demonstrate the Home Server Agent functionality +import { execSync } from 'child_process'; + +const API_BASE = 'http://localhost:3000'; +const API_TOKEN = 'dev-token-123'; + +console.log('🎮 Home Server Agent Test Script\n'); + +// Helper function to make API requests +function makeRequest(endpoint, method = 'GET', needsAuth = true) { + const url = `${API_BASE}${endpoint}`; + let cmd; + + if (process.platform === 'win32') { + // Windows PowerShell + if (needsAuth) { + cmd = `powershell "Invoke-RestMethod -Uri '${url}' -Method ${method} -Headers @{'Authorization'='Bearer ${API_TOKEN}'}"`; + } else { + cmd = `powershell "Invoke-RestMethod -Uri '${url}' -Method ${method}"`; + } + } else { + // Unix-like systems + if (needsAuth) { + cmd = `curl -s -X ${method} -H "Authorization: Bearer ${API_TOKEN}" ${url}`; + } else { + cmd = `curl -s -X ${method} ${url}`; + } + } + + try { + const result = execSync(cmd, { encoding: 'utf8' }); + return result; + } catch (error) { + console.error(`Error making request to ${endpoint}:`, error.message); + return null; + } +} + +// Test sequence +console.log('1. Testing health check (no auth required)...'); +const healthResult = makeRequest('/health', 'GET', false); +console.log(' ✅ Health check result:', healthResult ? 'OK' : 'FAILED'); + +console.log('\n2. Testing server status...'); +const statusResult = makeRequest('/api/status'); +console.log(' ✅ Server status retrieved'); + +console.log('\n3. Testing game server list...'); +const listResult = makeRequest('/api/gameserver/list'); +console.log(' ✅ Available game servers retrieved'); + +console.log('\n4. Testing API documentation...'); +const rootResult = makeRequest('/', 'GET', false); +console.log(' ✅ API documentation retrieved'); + +console.log('\n5. Testing specific game server status...'); +const minecraftStatus = makeRequest('/api/gameserver/minecraft/status'); +console.log(' ✅ Minecraft server status retrieved'); + +console.log('\n🎉 All tests completed successfully!'); +console.log('\nNext steps:'); +console.log('- Start a game server: POST /api/gameserver/start/minecraft'); +console.log('- Stop a game server: POST /api/gameserver/stop/minecraft'); +console.log('- Check active ports: GET /api/status/ports'); +console.log('\nNote: Game servers require Docker to be running.'); diff --git a/node/tsconfig.json b/node/tsconfig.json new file mode 100644 index 0000000..415e6c8 --- /dev/null +++ b/node/tsconfig.json @@ -0,0 +1,43 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "node", + "lib": ["ES2022"], + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "allowSyntheticDefaultImports": true, + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "removeComments": true, + "noImplicitAny": true, + "strictNullChecks": true, + "strictFunctionTypes": true, + "noImplicitThis": true, + "noImplicitReturns": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "allowUnusedLabels": false, + "allowUnreachableCode": false, + "exactOptionalPropertyTypes": true, + "noImplicitOverride": true, + "noPropertyAccessFromIndexSignature": false, + "noUncheckedSideEffectImports": false, + "types": ["node", "bun-types"] + }, + "include": [ + "src/**/*" + ], + "exclude": [ + "node_modules", + "dist", + "**/*.test.ts", + "**/*.spec.ts" + ] +}